diff --git a/build.gradle.kts b/build.gradle.kts index 5249b6b94..ea1927b99 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ fun prop(name: String): String = ?: error("Property `$name` is not defined in gradle.properties for environment `$shortPlatformVersion`") val shortPlatformVersion = prop("shortPlatformVersion") -val codeVersion = "1.35.0" +val codeVersion = "1.36.0" val pluginVersion = "$codeVersion.$shortPlatformVersion" val pluginGroup = "org.move" val javaVersion = JavaVersion.VERSION_17 @@ -21,6 +21,8 @@ val pluginJarName = "intellij-move-$pluginVersion" val kotlinReflectVersion = "1.8.10" val aptosVersion = "3.1.0" +val remoteRobotVersion = "0.11.22" + group = pluginGroup version = pluginVersion @@ -254,6 +256,41 @@ project(":plugin") { } } } + + downloadRobotServerPlugin { + version.set(remoteRobotVersion) + } + + runIdeForUiTests { + systemProperty("robot-server.port", "8082") +// systemProperty "ide.mac.message.dialogs.as.sheets", "false" +// systemProperty "jb.privacy.policy.text", "" +// systemProperty "jb.consents.confirmation.enabled", "false" +// systemProperty "ide.mac.file.chooser.native", "false" +// systemProperty "jbScreenMenuBar.enabled", "false" +// systemProperty "apple.laf.useScreenMenuBar", "false" + systemProperty("idea.trust.all.projects", "true") + systemProperty("ide.show.tips.on.startup.default.value", "false") + } + } +} + +project(":ui-tests") { + dependencies { + implementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion") + implementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion") + implementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0") + + implementation("com.automation-remarks:video-recorder-junit5:2.0") + } + + tasks.named("test") { + useJUnitPlatform() } } diff --git a/plugin/src/main/resources/META-INF/plugin.xml b/plugin/src/main/resources/META-INF/plugin.xml index e31c816c6..3f36a544b 100644 --- a/plugin/src/main/resources/META-INF/plugin.xml +++ b/plugin/src/main/resources/META-INF/plugin.xml @@ -1,4 +1,7 @@ - + + org.move.lang Move Language diff --git a/settings.gradle.kts b/settings.gradle.kts index 9b38bf18e..886c8a92e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,3 +8,4 @@ pluginManagement { } include("plugin") +include("ui-tests") diff --git a/src/main/grammars/MoveParser.bnf b/src/main/grammars/MoveParser.bnf index 38890bd6d..66d2543ac 100644 --- a/src/main/grammars/MoveParser.bnf +++ b/src/main/grammars/MoveParser.bnf @@ -596,7 +596,7 @@ private TypeParamBound_items ::= Ability ( '+' Ability )* } private TypeParamBound_items_recover ::= !('>' | ',') -TypeArgumentList ::= '<' <'))>>? '>' { +TypeArgumentList ::= '<' !'=' <'))>>? '>' { name = "type arguments" } TypeArgument ::= Type @@ -930,7 +930,10 @@ AddressLit ::= '@' AddressRef { pin = 1 } CallExpr ::= (Path &'(') ValueArgumentList { pin = 1 - implements = ["org.move.lang.core.psi.PathExpr"] + implements = [ + "org.move.lang.core.psi.PathExpr" + "org.move.lang.core.psi.ext.MvCallable" + ] } ValueArgumentList ::= '(' ValueArgumentList_items? ')' { pin = 1 @@ -972,17 +975,29 @@ Initializer ::= '=' Expr { pin = 1 } BorrowExpr ::= '&' mut? Expr -DotExpr ::= Expr DotExpr_field +DotExpr ::= Expr DotExpr_inner // Do not inline this rule, it breaks expression parsing -private DotExpr_field ::= '.' (!'.') StructDotField { +private DotExpr_inner ::= '.' !('.' | VectorStart) (MethodCall | StructDotField) { pin = 2 consumeTokenMethod = "consumeTokenFast" } -StructDotField ::= !(<> ('[' | '<')) IDENTIFIER !('(' | '::' | '!' | '{') + +private VectorStart ::= (<> ('[' | '<')) + +StructDotField ::= IDENTIFIER !('(' | '::' | '!' | '{') { - implements = ["org.move.lang.core.resolve.ref.MvMandatoryReferenceElement"] + implements = ["org.move.lang.core.psi.ext.MvMethodOrField"] mixin = "org.move.lang.core.psi.ext.MvStructDotFieldMixin" } +MethodCall ::= IDENTIFIER TypeArgumentList? ValueArgumentList +{ + implements = [ + "org.move.lang.core.psi.ext.MvMethodOrField" + "org.move.lang.core.psi.ext.MvMethodOrPath" + "org.move.lang.core.psi.ext.MvCallable" + ] + mixin = "org.move.lang.core.psi.ext.MvMethodCallMixin" +} IndexExpr ::= Expr IndexArg // Do not inline this rule, it breaks expression parsing @@ -994,7 +1009,10 @@ RefExpr ::= Path !'{' { Path ::= (ModulePathIdent | FQModulePathIdent | LocalPathIdent) TypeArgumentList? { - implements = ["org.move.lang.core.resolve.ref.MvPathReferenceElement"] + implements = [ + "org.move.lang.core.resolve.ref.MvPathReferenceElement" + "org.move.lang.core.psi.ext.MvMethodOrPath" + ] mixin = "org.move.lang.core.psi.ext.MvPathMixin" } @@ -1037,7 +1055,10 @@ NamedAddress ::= IDENTIFIER } /// Macros -AssertBangExpr ::= <> '!' ValueArgumentList { pin = 2 } +AssertBangExpr ::= <> '!' ValueArgumentList { + pin = 2 + implements = [ "org.move.lang.core.psi.ext.MvCallable" ] +} //AssertBangExpr ::= MacroIdent ValueArgumentList { pin = 1 } //MacroIdent ::= IDENTIFIER '!' diff --git a/src/main/kotlin/org/move/cli/LibraryRootsProvider.kt b/src/main/kotlin/org/move/cli/LibraryRootsProvider.kt index 8b8db6e36..9a356b818 100644 --- a/src/main/kotlin/org/move/cli/LibraryRootsProvider.kt +++ b/src/main/kotlin/org/move/cli/LibraryRootsProvider.kt @@ -62,8 +62,10 @@ private val MoveProject.ideaLibraries: Collection } .map { val sourceRoots = it.layoutPaths().mapNotNull { p -> p.toVirtualFile() }.toMutableSet() - it.moveToml.tomlFile - ?.virtualFile?.let { f -> sourceRoots.add(f) } + val tomlFile = it.moveToml.tomlFile.virtualFile + sourceRoots.add(tomlFile) +// it.moveToml.tomlFile +// .virtualFile.let { f -> sourceRoots.add(f) } MoveLangLibrary(it.packageName, sourceRoots, emptySet(), MoveIcons.MOVE_LOGO, null) } diff --git a/src/main/kotlin/org/move/cli/MovePackage.kt b/src/main/kotlin/org/move/cli/MovePackage.kt index 266deeeee..e575ec8b5 100644 --- a/src/main/kotlin/org/move/cli/MovePackage.kt +++ b/src/main/kotlin/org/move/cli/MovePackage.kt @@ -5,7 +5,6 @@ import com.intellij.openapi.vfs.VirtualFile import org.move.cli.manifest.AptosConfigYaml import org.move.cli.manifest.MoveToml import org.move.lang.toNioPathOrNull -import org.move.openapiext.checkReadAccessAllowed import org.move.openapiext.pathAsPath import org.move.openapiext.resolveExisting import java.nio.file.Path @@ -14,13 +13,29 @@ data class MovePackage( val project: Project, val contentRoot: VirtualFile, val moveToml: MoveToml, - val aptosConfigYaml: AptosConfigYaml?, ) { val packageName = this.moveToml.packageName ?: "" val sourcesFolder: VirtualFile? get() = contentRoot.takeIf { it.isValid }?.findChild("sources") val testsFolder: VirtualFile? get() = contentRoot.takeIf { it.isValid }?.findChild("tests") + val aptosConfigYaml: AptosConfigYaml? + get() { + var root: VirtualFile? = contentRoot + while (true) { + if (root == null) break + val candidatePath = root + .findChild(".aptos") + ?.takeIf { it.isDirectory } + ?.findChild("config.yaml") + if (candidatePath != null) { + return AptosConfigYaml.fromPath(candidatePath.pathAsPath) + } + root = root.parent + } + return null + } + fun moveFolders(): List = listOfNotNull(sourcesFolder, testsFolder) fun layoutPaths(): List { @@ -48,24 +63,9 @@ data class MovePackage( } companion object { - fun fromMoveToml(moveToml: MoveToml): MovePackage? { - checkReadAccessAllowed() - val contentRoot = moveToml.tomlFile?.parent?.virtualFile ?: return null - - var aptosConfigYaml: AptosConfigYaml? = null - var root: VirtualFile? = contentRoot - while (true) { - if (root == null) break - val candidatePath = root - .findChild(".aptos") - ?.takeIf { it.isDirectory } - ?.findChild("config.yaml") - if (candidatePath != null) { - aptosConfigYaml = AptosConfigYaml.fromPath(candidatePath.pathAsPath) ?: break - } - root = root.parent - } - return MovePackage(moveToml.project, contentRoot, moveToml, aptosConfigYaml) + fun fromMoveToml(moveToml: MoveToml): MovePackage { + val contentRoot = moveToml.tomlFile.virtualFile.parent + return MovePackage(moveToml.project, contentRoot, moveToml) } } } diff --git a/src/main/kotlin/org/move/cli/MoveProject.kt b/src/main/kotlin/org/move/cli/MoveProject.kt index 666cd86c2..27b6d72b2 100644 --- a/src/main/kotlin/org/move/cli/MoveProject.kt +++ b/src/main/kotlin/org/move/cli/MoveProject.kt @@ -3,6 +3,7 @@ package org.move.cli import com.intellij.openapi.project.Project import com.intellij.openapi.util.UserDataHolderBase import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiFileFactory import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.GlobalSearchScopes import com.intellij.psi.util.CachedValueProvider @@ -11,8 +12,10 @@ import com.intellij.psi.util.PsiModificationTracker import org.move.cli.manifest.AptosConfigYaml import org.move.cli.manifest.MoveToml import org.move.lang.MoveFile +import org.move.lang.core.psi.MvModule import org.move.lang.core.types.Address import org.move.lang.core.types.AddressLit +import org.move.lang.index.MvNamedElementIndex import org.move.lang.toMoveFile import org.move.lang.toNioPathOrNull import org.move.openapiext.common.checkUnitTestMode @@ -20,13 +23,15 @@ import org.move.openapiext.contentRoots import org.move.stdext.chain import org.move.stdext.iterateMoveVirtualFiles import org.move.stdext.wrapWithList +import org.toml.lang.TomlLanguage +import org.toml.lang.psi.TomlFile import java.nio.file.Path data class MoveProject( val project: Project, val currentPackage: MovePackage, val dependencies: List>, -) : UserDataHolderBase() { +): UserDataHolderBase() { val contentRoot: VirtualFile get() = this.currentPackage.contentRoot val contentRootPath: Path? get() = this.currentPackage.contentRoot.toNioPathOrNull() @@ -101,6 +106,12 @@ data class MoveProject( return searchScope } + fun getModulesFromIndex(name: String): Collection { + return MvNamedElementIndex + .getElementsByName(project, name, searchScope()) + .filterIsInstance() + } + val aptosConfigYaml: AptosConfigYaml? get() = this.currentPackage.aptosConfigYaml val profiles: Set = this.aptosConfigYaml?.profiles.orEmpty() @@ -129,8 +140,18 @@ data class MoveProject( fun forTests(project: Project): MoveProject { checkUnitTestMode() val contentRoot = project.contentRoots.first() - val moveToml = MoveToml(project) - val movePackage = MovePackage(project, contentRoot, moveToml, null) + val tomlFile = + PsiFileFactory.getInstance(project) + .createFileFromText( + TomlLanguage, + """ + [package] + name = "MyPackage" + """ + ) as TomlFile + + val moveToml = MoveToml(project, tomlFile) + val movePackage = MovePackage(project, contentRoot, moveToml) return MoveProject( project, movePackage, diff --git a/src/main/kotlin/org/move/cli/MoveProjectsService.kt b/src/main/kotlin/org/move/cli/MoveProjectsService.kt index 444de64df..08da60768 100644 --- a/src/main/kotlin/org/move/cli/MoveProjectsService.kt +++ b/src/main/kotlin/org/move/cli/MoveProjectsService.kt @@ -7,7 +7,10 @@ import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.externalSystem.autoimport.AutoImportProjectTracker +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTracker import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.project.Project import com.intellij.openapi.project.ex.ProjectEx import com.intellij.openapi.roots.ModuleRootModificationUtil @@ -16,7 +19,6 @@ import com.intellij.openapi.roots.ex.ProjectRootManagerEx import com.intellij.openapi.util.EmptyRunnable import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.ex.temp.TempFileSystem import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiElement @@ -24,9 +26,9 @@ import com.intellij.psi.PsiFile import com.intellij.psi.PsiInvalidElementAccessException import com.intellij.psi.util.parents import com.intellij.util.messages.Topic -import org.move.cli.settings.MoveProjectSettingsService -import org.move.cli.settings.MoveSettingsChangedEvent -import org.move.cli.settings.MoveSettingsListener +import org.move.cli.externalSystem.MoveExternalSystemProjectAware +import org.move.cli.settings.MvProjectSettingsServiceBase.* +import org.move.cli.settings.MvProjectSettingsServiceBase.Companion.MOVE_SETTINGS_TOPIC import org.move.cli.settings.debugErrorOrFallback import org.move.lang.core.psi.ext.elementType import org.move.lang.toNioPathOrNull @@ -40,6 +42,7 @@ import org.move.stdext.FileToMoveProjectCache import java.io.File import java.nio.file.Path import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException val Project.moveProjectsService get() = service() @@ -47,32 +50,18 @@ val Project.hasMoveProject get() = this.moveProjectsService.allProjects.isNotEmp class MoveProjectsService(val project: Project): Disposable { - private val refreshOnBuildDirChangeWatcher = - BuildDirectoryWatcher(emptyList()) { scheduleProjectsRefresh("build/ directory changed") } +// private val refreshOnBuildDirChangeWatcher = +// BuildDirectoryWatcher(emptyList()) { scheduleProjectsRefresh("build/ directory changed") } var initialized = false init { - with(project.messageBus.connect()) { - if (!isUnitTestMode) { - subscribe(VirtualFileManager.VFS_CHANGES, refreshOnBuildDirChangeWatcher) - subscribe(VirtualFileManager.VFS_CHANGES, MoveTomlWatcher { - // on every Move.toml change - // TODO: move to External system integration - scheduleProjectsRefresh("Move.toml changed") - }) - } - subscribe(MoveProjectSettingsService.MOVE_SETTINGS_TOPIC, object: MoveSettingsListener { - override fun moveSettingsChanged(e: MoveSettingsChangedEvent) { - // on every Move Language plugin settings change - scheduleProjectsRefresh("plugin settings changed") - } - }) - } + registerProjectAware(project, this) } - val allProjects: List - get() = this.projects.state + val allProjects: List get() = this.projects.state + + val hasAtLeastOneValidProject: Boolean get() = this.allProjects.isNotEmpty() fun scheduleProjectsRefresh(reason: String? = null): CompletableFuture> { LOG.logProjectsRefresh("scheduled", reason) @@ -83,6 +72,32 @@ class MoveProjectsService(val project: Project): Disposable { return moveProjectsFut } + private fun registerProjectAware(project: Project, disposable: Disposable) { + // There is no sense to register `CargoExternalSystemProjectAware` for default project. + // Moreover, it may break searchable options building. + // Also, we don't need to register `CargoExternalSystemProjectAware` in light tests because: + // - we check it only in heavy tests + // - it heavily depends on service disposing which doesn't work in light tests + if (project.isDefault || isUnitTestMode && (project as? ProjectEx)?.isLight == true) return + + val moveProjectAware = MoveExternalSystemProjectAware(project) + val projectTracker = ExternalSystemProjectTracker.getInstance(project) + projectTracker.register(moveProjectAware, disposable) + projectTracker.activate(moveProjectAware.projectId) + + @Suppress("UnstableApiUsage") + project.messageBus.connect(disposable) + .subscribe(MOVE_SETTINGS_TOPIC, object: MoveSettingsListener { + override fun > settingsChanged(e: SettingsChangedEventBase) { + if (e.affectsMoveProjectsMetadata) { + val tracker = AutoImportProjectTracker.getInstance(project) + tracker.markDirty(moveProjectAware.projectId) + tracker.scheduleProjectRefresh() + } + } + }) + } + // requires ReadAccess fun findMoveProjectForPsiElement(psiElement: PsiElement): MoveProject? { // read access required for the psiElement.containingFile @@ -122,8 +137,10 @@ class MoveProjectsService(val project: Project): Disposable { private fun doRefreshProjects(project: Project, reason: String?): CompletableFuture> { val moveProjectsFut = CompletableFuture>() + val syncTask = MoveProjectsSyncTask(project, moveProjectsFut, reason) project.taskQueue.run(syncTask) + return moveProjectsFut.thenApply { updatedProjects -> runOnlyInNonLightProject(project) { setupProjectRoots(project, updatedProjects) @@ -179,14 +196,18 @@ class MoveProjectsService(val project: Project): Disposable { * go through this method: it makes sure that when we update various IDEA listeners. */ private fun modifyProjectModel( - updater: (List) -> CompletableFuture> + modifyProjects: (List) -> CompletableFuture> ): CompletableFuture> { - val wrappedUpdater = { projects: List -> - updater(projects) + val refreshStatusPublisher = + project.messageBus.syncPublisher(MoveProjectsService.MOVE_PROJECTS_REFRESH_TOPIC) + + val wrappedModifyProjects = { projects: List -> + refreshStatusPublisher.onRefreshStarted() + modifyProjects(projects) } - return projects.updateAsync(wrappedUpdater) + return projects.updateAsync(wrappedModifyProjects) .thenApply { projects -> - refreshOnBuildDirChangeWatcher.setWatchedProjects(projects) +// refreshOnBuildDirChangeWatcher.setWatchedProjects(projects) invokeAndWaitIfNeeded { runWriteAction { // remove file -> moveproject associations from cache @@ -209,6 +230,19 @@ class MoveProjectsService(val project: Project): Disposable { } projects } + .handle { projects, err -> + val status = err?.toRefreshStatus() ?: MoveRefreshStatus.SUCCESS + refreshStatusPublisher.onRefreshFinished(status) + projects + } + } + + private fun Throwable.toRefreshStatus(): MoveRefreshStatus { + return when { + this is ProcessCanceledException -> MoveRefreshStatus.CANCEL + this is CompletionException && cause is ProcessCanceledException -> MoveRefreshStatus.CANCEL + else -> MoveRefreshStatus.FAILURE + } } override fun dispose() {} @@ -220,11 +254,28 @@ class MoveProjectsService(val project: Project): Disposable { "move projects changes", MoveProjectsListener::class.java ) + + val MOVE_PROJECTS_REFRESH_TOPIC: Topic = Topic( + "Move Projects refresh", + MoveProjectsRefreshListener::class.java + ) + } fun interface MoveProjectsListener { fun moveProjectsUpdated(service: MoveProjectsService, projects: Collection) } + + interface MoveProjectsRefreshListener { + fun onRefreshStarted() + fun onRefreshFinished(status: MoveRefreshStatus) + } + + enum class MoveRefreshStatus { + SUCCESS, + FAILURE, + CANCEL + } } fun setupProjectRoots(project: Project, moveProjects: List) { diff --git a/src/main/kotlin/org/move/cli/MoveProjectsSyncTask.kt b/src/main/kotlin/org/move/cli/MoveProjectsSyncTask.kt index 43a1373ee..6a0711471 100644 --- a/src/main/kotlin/org/move/cli/MoveProjectsSyncTask.kt +++ b/src/main/kotlin/org/move/cli/MoveProjectsSyncTask.kt @@ -1,85 +1,267 @@ +@file:Suppress("UnstableApiUsage") + package org.move.cli +import com.intellij.build.BuildContentDescriptor +import com.intellij.build.BuildDescriptor +import com.intellij.build.DefaultBuildDescriptor +import com.intellij.build.SyncViewManager +import com.intellij.build.events.BuildEventsNls +import com.intellij.build.events.MessageEvent +import com.intellij.build.progress.BuildProgress +import com.intellij.build.progress.BuildProgressDescriptor +import com.intellij.execution.process.ProcessAdapter +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.runReadAction import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Computable -import com.intellij.psi.PsiDocumentManager +import com.intellij.openapi.util.NlsContexts +import com.intellij.openapi.vfs.VirtualFile import org.move.cli.manifest.MoveToml -import org.move.cli.manifest.TomlDependency +import org.move.cli.settings.Blockchain.APTOS +import org.move.cli.settings.Blockchain.SUI +import org.move.cli.settings.blockchain +import org.move.cli.settings.suiCli import org.move.lang.toNioPathOrNull import org.move.lang.toTomlFile +import org.move.openapiext.TaskResult import org.move.openapiext.contentRoots import org.move.openapiext.resolveExisting import org.move.openapiext.toVirtualFile import org.move.stdext.iterateFiles +import org.move.stdext.unwrapOrElse import org.move.stdext.withExtended +import java.nio.file.Path import java.util.concurrent.CompletableFuture +import javax.swing.JComponent class MoveProjectsSyncTask( project: Project, private val future: CompletableFuture>, private val reason: String? -) : Task.Backgroundable(project, "Reloading Move packages", true) { +): Task.Backgroundable(project, "Reloading Move packages", true) { override fun run(indicator: ProgressIndicator) { indicator.isIndeterminate = true val before = System.currentTimeMillis() LOG.logProjectsRefresh("started", reason) - // aptos move fetch - fetchDependencies() - - val moveProjects = PsiDocumentManager - .getInstance(project) - .commitAndRunReadAction(Computable { loadProjects(project) }) - LOG.logProjectsRefresh("finished in ${System.currentTimeMillis() - before}ms", reason) - future.complete(moveProjects) - } - private fun fetchDependencies() { - // run `aptos move fetch` here + val syncProgress = SyncViewManager.createBuildProgress(project) + + val refreshedProjects = try { + syncProgress.start(createSyncProgressDescriptor(indicator)) + + val refreshedProjects = doRun(indicator, syncProgress) + + // TODO: +// val isUpdateFailed = refreshedProjects.any { it.mergedStatus is CargoProject.UpdateStatus.UpdateFailed } +// if (isUpdateFailed) { +// syncProgress.fail() +// } else { +// syncProgress.finish() +// } + syncProgress.finish() + + refreshedProjects + } catch (e: Throwable) { + if (e is ProcessCanceledException) { + syncProgress.cancel() + } else { + syncProgress.fail() + } + future.completeExceptionally(e) + throw e + } + future.complete(refreshedProjects) + + val elapsed = System.currentTimeMillis() - before + LOG.logProjectsRefresh("finished Move projects Sync Task in $elapsed ms", reason) } - companion object { - private val LOG = logger() + private fun doRun( + indicator: ProgressIndicator, + syncProgress: BuildProgress + ): List { + val moveProjects = mutableListOf() - private data class DepId(val rootPath: String) + for (contentRoot in project.contentRoots) { + contentRoot.iterateFiles({ it.name == Consts.MANIFEST_FILE }) { moveTomlFile -> + indicator.checkCanceled() + + val projectDirName = moveTomlFile.parent.name + syncProgress.runWithChildProgress( + "Sync $projectDirName project", + createContext = { it }, + action = { childProgress -> + val context = SyncContext(project, indicator, syncProgress.id, childProgress) + loadProject( + moveTomlFile, projects = moveProjects, context = context + ) + } + ) + + true + } + } - fun loadProjects(project: Project): List { - val projects = mutableListOf() - for (contentRoot in project.contentRoots) { - contentRoot.iterateFiles({ it.name == Consts.MANIFEST_FILE }) { - val rawDepQueue = ArrayDeque>() - val root = it.parent?.toNioPathOrNull() ?: return@iterateFiles true - val tomlFile = it.toTomlFile(project) ?: return@iterateFiles true + return moveProjects + } + + private fun loadProject( + moveTomlFile: VirtualFile, + projects: MutableList, + context: SyncContext + ) { + val projectRoot = moveTomlFile.parent?.toNioPathOrNull() ?: error("cannot be invalid path") - val moveToml = MoveToml.fromTomlFile(tomlFile, root) - rawDepQueue.addAll(moveToml.deps) + context.runWithChildProgress("Fetching dependency packages") { childContext -> + fetchProjectDependencies( + projectRoot, listener = SyncProcessAdapter(childContext) + ) + TaskResult.Ok(Unit) + } - val rootPackage = MovePackage.fromMoveToml(moveToml) ?: return@iterateFiles true + val (rootPackage, deps) = + (context.runWithChildProgress("Loading dependencies") { childContext -> + // Blocks till completed or cancelled by the toml / file change + runReadAction { + val tomlFile = moveTomlFile.toTomlFile(project)!! + val moveToml = MoveToml.fromTomlFile(tomlFile, projectRoot) + val rootPackage = MovePackage.fromMoveToml(moveToml) val deps = mutableListOf>() - val visitedDepIds = mutableSetOf( - DepId(rootPackage.contentRoot.path) - ) - loadDependencies(project, moveToml, deps, visitedDepIds, true) + val visitedDepIds = mutableSetOf(DepId(rootPackage.contentRoot.path)) + loadDependencies(project, moveToml, deps, visitedDepIds, true, childContext.progress) + + TaskResult.Ok(Pair(rootPackage, deps)) + } + } as TaskResult.Ok).value + + val moveProject = MoveProject(project, rootPackage, deps) + projects.add(moveProject) + } + + private fun fetchProjectDependencies(projectDir: Path, listener: ProcessProgressListener) { + when (project.blockchain) { + SUI -> { + val suiCli = project.suiCli + if (suiCli == null) { + listener.error("Invalid Sui CLI configuration", "") + return + } + suiCli.fetchPackageDependencies( + projectDir, + owner = project, + processListener = listener + ).unwrapOrElse { + listener.error("Failed to fetch dependencies", it.message.orEmpty()) + } + } + APTOS -> { + // TODO: not supported by CLI yet + } + } + } + + private fun createSyncProgressDescriptor(progress: ProgressIndicator): BuildProgressDescriptor { + val buildContentDescriptor = BuildContentDescriptor( + null, + null, + object: JComponent() {}, + "Move" + ) + buildContentDescriptor.isActivateToolWindowWhenFailed = true + buildContentDescriptor.isActivateToolWindowWhenAdded = false +// buildContentDescriptor.isNavigateToError = project.rustSettings.autoShowErrorsInEditor + val refreshAction = ActionManager.getInstance().getAction("Move.RefreshAllProjects") + val descriptor = DefaultBuildDescriptor(Any(), "Move", project.basePath!!, System.currentTimeMillis()) + .withContentDescriptor { buildContentDescriptor } + .withRestartAction(refreshAction) + .withRestartAction(StopAction(progress)) + return object: BuildProgressDescriptor { + override fun getTitle(): String = descriptor.title + override fun getBuildDescriptor(): BuildDescriptor = descriptor + } + } + + private class StopAction(private val progress: ProgressIndicator): + DumbAwareAction({ "Stop" }, AllIcons.Actions.Suspend) { + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = progress.isRunning + } + + override fun actionPerformed(e: AnActionEvent) { + progress.cancel() + } + } + + data class SyncContext( + val project: Project, +// val toolchain: RsToolchainBase, + val progress: ProgressIndicator, + val buildId: Any, + val syncProgress: BuildProgress + ) { - projects.add(MoveProject(project, rootPackage, deps)) - true + val id: Any get() = syncProgress.id + + fun runWithChildProgress( + @NlsContexts.ProgressText title: String, + action: (SyncContext) -> TaskResult + ): TaskResult { + progress.checkCanceled() + progress.text = title + + return syncProgress.runWithChildProgress( + title, + { copy(syncProgress = it) }, + action + ) { childProgress, result -> + when (result) { + is TaskResult.Ok -> childProgress.finish() + is TaskResult.Err -> { + childProgress.message( + result.reason, + result.message.orEmpty(), + MessageEvent.Kind.ERROR, + null + ) + childProgress.fail() + } } } - return projects } + fun withProgressText(@NlsContexts.ProgressText @NlsContexts.ProgressTitle text: String) { + progress.text = text + syncProgress.progress(text) + } + } + + companion object { + private val LOG = logger() + + private data class DepId(val rootPath: String) + private fun loadDependencies( project: Project, rootMoveToml: MoveToml, deps: MutableList>, visitedIds: MutableSet, isRoot: Boolean, + progress: ProgressIndicator ) { + // checks for the cancel() of the whole SyncTask + progress.checkCanceled() + var parsedDeps = rootMoveToml.deps if (isRoot) { parsedDeps = parsedDeps.withExtended(rootMoveToml.dev_deps) @@ -97,14 +279,75 @@ class MoveProjectsSyncTask( val depMoveToml = MoveToml.fromTomlFile(depTomlFile, depRoot) // first try to parse MovePackage from dependency, no need for nested if parent is invalid - val depPackage = MovePackage.fromMoveToml(depMoveToml) ?: continue + val depPackage = MovePackage.fromMoveToml(depMoveToml) // parse all nested dependencies with their address maps visitedIds.add(depId) - loadDependencies(project, depMoveToml, deps, visitedIds, false) + loadDependencies(project, depMoveToml, deps, visitedIds, false, progress) deps.add(Pair(depPackage, addressMap)) } } } } + +private class SyncProcessAdapter( + private val context: MoveProjectsSyncTask.SyncContext +): ProcessAdapter(), + ProcessProgressListener { +// override fun onTextAvailable(event: ProcessEvent, outputType: Key) { +// val text = event.text.trim { it <= ' ' } +// if (text.startsWith("Updating") || text.startsWith("Downloading")) { +// context.withProgressText(text) +// } +// if (text.startsWith("Vendoring")) { +// // This code expect that vendoring message has the following format: +// // "Vendoring %package_name% v%package_version% (%src_dir%) to %dst_dir%". +// // So let's extract "Vendoring %package_name% v%package_version%" part and show it for users +// val index = text.indexOf(" (") +// val progressText = if (index != -1) text.substring(0, index) else text +// context.withProgressText(progressText) +// } +// } + + override fun error(title: String, message: String) = context.error(title, message) + override fun warning(title: String, message: String) = context.warning(title, message) +} + +private fun BuildProgress.runWithChildProgress( + @BuildEventsNls.Title title: String, + createContext: (BuildProgress) -> T, + action: (T) -> R, + onResult: (BuildProgress, R) -> Unit = { progress, _ -> progress.finish() } +): R { + val childProgress = startChildProgress(title) + try { + val context = createContext(childProgress) + val result = action(context) + onResult(childProgress, result) + return result + } catch (e: Throwable) { + if (e is ProcessCanceledException) { + cancel() + } else { + fail() + } + throw e + } +} + +private fun MoveProjectsSyncTask.SyncContext.error( + @BuildEventsNls.Title title: String, + @BuildEventsNls.Message message: String +) { + syncProgress.message(title, message, com.intellij.build.events.MessageEvent.Kind.ERROR, null) +} + +private fun MoveProjectsSyncTask.SyncContext.warning( + @BuildEventsNls.Title title: String, + @BuildEventsNls.Message message: String +) { + syncProgress.message(title, message, com.intellij.build.events.MessageEvent.Kind.WARNING, null) +} + + diff --git a/src/main/kotlin/org/move/cli/ProcessProgressListener.kt b/src/main/kotlin/org/move/cli/ProcessProgressListener.kt new file mode 100644 index 000000000..6fd5bd4b3 --- /dev/null +++ b/src/main/kotlin/org/move/cli/ProcessProgressListener.kt @@ -0,0 +1,15 @@ +/* + * Use of this source code is governed by the MIT license that can be + * found in the LICENSE file. + */ + +package org.move.cli + +import com.intellij.build.events.BuildEventsNls +import com.intellij.execution.process.ProcessListener + +@Suppress("UnstableApiUsage") +interface ProcessProgressListener : ProcessListener { + fun error(@BuildEventsNls.Title title: String, @BuildEventsNls.Message message: String) + fun warning(@BuildEventsNls.Title title: String, @BuildEventsNls.Message message: String) +} diff --git a/src/main/kotlin/org/move/cli/download/AptosDownload.kt b/src/main/kotlin/org/move/cli/download/AptosDownload.kt new file mode 100644 index 000000000..ccc670b5e --- /dev/null +++ b/src/main/kotlin/org/move/cli/download/AptosDownload.kt @@ -0,0 +1,175 @@ +package org.move.cli.download + +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.projectRoots.impl.jdkDownloader.JdkDownloaderLogger +import com.intellij.openapi.util.io.FileUtil +import com.intellij.util.Urls +import com.intellij.util.io.Decompressor +import com.intellij.util.io.HttpRequests +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.isRegularFile + +data class AptosItem(val version: String) { + val url + get() = + "https://github.com/aptos-labs/aptos-core/releases/download/aptos-cli-v$version/aptos-cli-$version-$currentPlatform-x86_64.zip" + val archiveVersionInfo get() = "$version-$currentPlatform-x86_64.zip" + + val executableName get() = "aptos-cli-$version" + val archiveNameNoExtension get() = "aptos-cli-$version-$currentPlatform" + + val path: Path get() = AptosInstaller.installDir.resolve(executableName) + + override fun toString(): String = executableName + + companion object { + private val currentPlatform: String + get() { + return "Ubuntu-22.04" + } + } +} + +@Service +class AptosInstaller { + fun installAptosCli(aptosItem: AptosItem, indicator: ProgressIndicator, onFinish: (AptosItem) -> Unit = { _ -> }) { + indicator.text = "Installing ${aptosItem.archiveNameNoExtension}..." + + val url = Urls.parse(aptosItem.url, false) ?: error("Cannot parse download URL: ${aptosItem.url}") + if (!url.scheme.equals("https", ignoreCase = true)) { + error("URL must use https:// protocol, but was: $url") + } + + indicator.text2 = "Downloading" + val tempDownloadFile = + Paths.get( + PathManager.getTempPath(), + FileUtil.sanitizeFileName("aptos-cli-${System.nanoTime()}-${aptosItem.archiveVersionInfo}") + ) + val tempExtractionDir = tempDownloadFile.parent.resolve(aptosItem.archiveNameNoExtension) + val targetFilePath = aptosItem.path + try { + try { + HttpRequests.request(aptosItem.url) + .productNameAsUserAgent() + .saveToFile(tempDownloadFile.toFile(), indicator) + + if (!tempDownloadFile.isRegularFile()) { + throw RuntimeException("Downloaded file does not exist: $tempDownloadFile") + } + } catch (t: Throwable) { + if (t is ControlFlowException) throw t + throw RuntimeException( + "Failed to download ${aptosItem.archiveNameNoExtension} from $url. ${t.message}", + t + ) + } + +// val sizeDiff = runCatching { Files.size(downloadFile) - item.archiveSize }.getOrNull() +// if (sizeDiff != 0L) { +// throw RuntimeException("The downloaded ${item.fullPresentationText} has incorrect file size,\n" + +// "the difference is ${sizeDiff?.absoluteValue ?: "unknown" } bytes.\n" + +// "Check your internet connection and try again later") +// } + +// val actualHashCode = runCatching { com.google.common.io.Files.asByteSource(downloadFile.toFile()).hash( +// Hashing.sha256()).toString() }.getOrNull() +// if (!actualHashCode.equals(item.sha256, ignoreCase = true)) { +// throw RuntimeException("Failed to verify SHA-256 checksum for ${item.fullPresentationText}\n\n" + +// "The actual value is ${actualHashCode ?: "unknown"},\n" + +// "but expected ${item.sha256} was expected\n" + +// "Check your internet connection and try again later") +// } + + indicator.isIndeterminate = true + indicator.text2 = "Unpacking" + + try { +// if (wslDistribution != null) { +// JdkInstallerWSL.unpackJdkOnWsl( +// wslDistribution, +// item.packageType, +// downloadFile, +// targetDir, +// item.packageRootPrefix +// ) +// } +// else { + + tempExtractionDir.toFile().mkdir() + + Decompressor.Zip(tempDownloadFile).withZipExtensions() + .entryFilter { indicator.checkCanceled(); true } +// .let { +// val fullMatchPath = item.packageRootPrefix.trim('/') +// if (fullMatchPath.isBlank()) it else it.removePrefixPath(fullMatchPath) +// } + .extract(tempExtractionDir) + +// runCatching { writeMarkerFile(item) } +// JdkDownloaderLogger.logDownload(true) + } catch (t: Throwable) { + if (t is ControlFlowException) throw t + throw RuntimeException("Failed to extract ${aptosItem.archiveNameNoExtension}. ${t.message}", t) + } + + try { + FileUtil.copy(tempExtractionDir.resolve("aptos").toFile(), targetFilePath.toFile()) + + } catch (t: Throwable) { + if (t is ControlFlowException) throw t + throw RuntimeException("Failed to copy to ${targetFilePath}. ${t.message}", t) + } + + onFinish(aptosItem) + + } catch (t: Throwable) { + //if we were cancelled in the middle or failed, let's clean up + JdkDownloaderLogger.logDownload(false) +// targetDir.delete() +// markerFile(targetDir)?.delete() + throw t + } finally { + runCatching { FileUtil.delete(tempDownloadFile) } + runCatching { FileUtil.delete(tempExtractionDir) } + } + } + + companion object { + val installDir: Path get() = Paths.get(System.getProperty("user.home"), "aptos-cli") + } +} + +//class AptosDownloadTask(val aptosItem: AptosItem): SdkDownloadTask { +// override fun getSuggestedSdkName(): String = "aptos" +// override fun getPlannedHomeDir(): String = joinPath(arrayOf(System.getProperty("user.home"), "aptos-cli")) +// override fun getPlannedVersion(): String = "3.1.0" +// +// override fun doDownload(indicator: ProgressIndicator) { +// service().installAptosCli(aptosItem, indicator) +// } +//} + +//class AptosDownload: SdkDownload { +// override fun supportsDownload(sdkTypeId: SdkTypeId): Boolean { +// return true +// } +// +// override fun showDownloadUI( +// sdkTypeId: SdkTypeId, +// sdkModel: SdkModel, +// parentComponent: JComponent, +// selectedSdk: Sdk?, +// sdkCreatedCallback: Consumer +// ) { +// val dataContext = DataManager.getInstance().getDataContext(parentComponent) +// val project = CommonDataKeys.PROJECT.getData(dataContext) +// if (project?.isDisposed == true) return +// +// TODO("Not yet implemented") +// } +//} \ No newline at end of file diff --git a/src/main/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAware.kt b/src/main/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAware.kt new file mode 100644 index 000000000..b0be7d9ed --- /dev/null +++ b/src/main/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAware.kt @@ -0,0 +1,54 @@ +package org.move.cli.externalSystem + +import com.intellij.openapi.Disposable +import com.intellij.openapi.externalSystem.autoimport.* +import com.intellij.openapi.externalSystem.model.ProjectSystemId +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.project.Project +import org.move.cli.MoveProjectsService +import org.move.cli.MoveProjectsService.MoveRefreshStatus +import org.move.cli.moveProjectsService + +class MoveExternalSystemProjectAware( + private val project: Project +): + ExternalSystemProjectAware { + + override val projectId: ExternalSystemProjectId = ExternalSystemProjectId(MOVE_SYSTEM_ID, project.name) + + override val settingsFiles: Set + get() { + val settingsFilesService = MoveSettingsFilesService.getInstance(project) + // Always collect fresh settings files + return settingsFilesService.collectSettingsFiles() + } + + override fun reloadProject(context: ExternalSystemProjectReloadContext) { + FileDocumentManager.getInstance().saveAllDocuments() + project.moveProjectsService.scheduleProjectsRefresh() + } + + override fun subscribe(listener: ExternalSystemProjectListener, parentDisposable: Disposable) { + project.messageBus.connect(parentDisposable).subscribe( + MoveProjectsService.MOVE_PROJECTS_REFRESH_TOPIC, + object : MoveProjectsService.MoveProjectsRefreshListener { + override fun onRefreshStarted() { + listener.onProjectReloadStart() + } + + override fun onRefreshFinished(status: MoveRefreshStatus) { + val externalStatus = when (status) { + MoveRefreshStatus.SUCCESS -> ExternalSystemRefreshStatus.SUCCESS + MoveRefreshStatus.FAILURE -> ExternalSystemRefreshStatus.FAILURE + MoveRefreshStatus.CANCEL -> ExternalSystemRefreshStatus.CANCEL + } + listener.onProjectReloadFinish(externalStatus) + } + } + ) + } + + companion object { + val MOVE_SYSTEM_ID: ProjectSystemId = ProjectSystemId("Move") + } +} diff --git a/src/main/kotlin/org/move/cli/externalSystem/MoveSettingsFilesService.kt b/src/main/kotlin/org/move/cli/externalSystem/MoveSettingsFilesService.kt new file mode 100644 index 000000000..0fd77b235 --- /dev/null +++ b/src/main/kotlin/org/move/cli/externalSystem/MoveSettingsFilesService.kt @@ -0,0 +1,37 @@ +package org.move.cli.externalSystem + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import org.move.cli.Consts +import org.move.cli.MoveProject +import org.move.cli.moveProjectsService +import org.move.stdext.blankToNull + +@Service(Service.Level.PROJECT) +class MoveSettingsFilesService(private val project: Project) { + + fun collectSettingsFiles(): Set { + val out = mutableSetOf() + for (moveProject in project.moveProjectsService.allProjects) { + moveProject.collectSettingsFiles(out) + } + return out + } + + private fun MoveProject.collectSettingsFiles(out: MutableSet) { + for (movePackage in this.movePackages()) { + val root = movePackage.contentRoot.path + out.add("$root/${Consts.MANIFEST_FILE}") + + val packageName = movePackage.packageName.blankToNull() + if (packageName != null) { + out.add("$root/build/$packageName/BuildInfo.yaml") + } + } + } + + companion object { + fun getInstance(project: Project): MoveSettingsFilesService = project.service() + } +} diff --git a/src/main/kotlin/org/move/cli/manifest/MoveToml.kt b/src/main/kotlin/org/move/cli/manifest/MoveToml.kt index 2542ea557..45b002d3d 100644 --- a/src/main/kotlin/org/move/cli/manifest/MoveToml.kt +++ b/src/main/kotlin/org/move/cli/manifest/MoveToml.kt @@ -3,13 +3,14 @@ package org.move.cli.manifest import com.intellij.openapi.project.Project import org.move.cli.* import org.move.openapiext.* +import org.move.openapiext.common.isUnitTestMode import org.move.stdext.chain import org.toml.lang.psi.TomlFile import java.nio.file.Path class MoveToml( val project: Project, - val tomlFile: TomlFile? = null, + val tomlFile: TomlFile, val packageTable: MoveTomlPackageTable? = null, val addresses: RawAddressMap = mutableRawAddressMap(), @@ -46,6 +47,9 @@ class MoveToml( companion object { fun fromTomlFile(tomlFile: TomlFile, projectRoot: Path): MoveToml { + // needs read access for Toml + checkReadAccessAllowed() + val packageTomlTable = tomlFile.getTable("package") var packageTable: MoveTomlPackageTable? = null if (packageTomlTable != null) { diff --git a/src/main/kotlin/org/move/cli/projectAware/MoveExternalSystemProjectAware.kt b/src/main/kotlin/org/move/cli/projectAware/MoveExternalSystemProjectAware.kt deleted file mode 100644 index 490da3e4b..000000000 --- a/src/main/kotlin/org/move/cli/projectAware/MoveExternalSystemProjectAware.kt +++ /dev/null @@ -1,60 +0,0 @@ -package org.move.cli.projectAware - -//class MoveExternalSystemProjectAware( -// private val project: Project -//) : ExternalSystemProjectAware { -// -// override val projectId: ExternalSystemProjectId = ExternalSystemProjectId(MOVE_SYSTEM_ID, project.name) -// -// override val settingsFiles: Set -// get() { -// val settingsFilesService = MoveSettingsFilesService.getInstance(project) -// // Always collect fresh settings files -// return settingsFilesService.collectSettingsFiles().toSet() -// } -// -// override fun reloadProject(context: ExternalSystemProjectReloadContext) { -// FileDocumentManager.getInstance().saveAllDocuments() -// project.moveProjects.refreshAllProjects() -// } -// -// override fun subscribe(listener: ExternalSystemProjectListener, parentDisposable: Disposable) { -// project.messageBus.connect(parentDisposable).subscribe( -// MoveProjectsService.MOVE_PROJECTS_REFRESH_TOPIC, -// object : MoveProjectsService.MoveProjectsRefreshListener { -// override fun onRefreshStarted() { -// listener.onProjectReloadStart() -// } -// -// override fun onRefreshFinished(status: MoveProjectsService.MoveRefreshStatus) { -// val externalStatus = when (status) { -// MoveProjectsService.MoveRefreshStatus.SUCCESS -> ExternalSystemRefreshStatus.SUCCESS -// MoveProjectsService.MoveRefreshStatus.FAILURE -> ExternalSystemRefreshStatus.FAILURE -// MoveProjectsService.MoveRefreshStatus.CANCEL -> ExternalSystemRefreshStatus.CANCEL -// } -// listener.onProjectReloadFinish(externalStatus) -// } -// } -// ) -// } -// -// companion object { -// val MOVE_SYSTEM_ID: ProjectSystemId = ProjectSystemId("Move") -// -// fun register(project: Project, disposable: Disposable) { -// val moveProjectAware = MoveExternalSystemProjectAware(project) -// val projectTracker = ExternalSystemProjectTracker.getInstance(project) -// projectTracker.register(moveProjectAware, disposable) -// projectTracker.activate(moveProjectAware.projectId) -// -// project.messageBus.connect(disposable) -// .subscribe(MoveProjectSettingsService.MOVE_SETTINGS_TOPIC, -// object : MoveSettingsListener { -// override fun moveSettingsChanged(e: MoveSettingsChangedEvent) { -// AutoImportProjectTracker.getInstance(project) -// .markDirty(moveProjectAware.projectId) -// } -// }) -// } -// } -//} diff --git a/src/main/kotlin/org/move/cli/projectAware/MoveSettingsFilesService.kt b/src/main/kotlin/org/move/cli/projectAware/MoveSettingsFilesService.kt deleted file mode 100644 index 82bdb506c..000000000 --- a/src/main/kotlin/org/move/cli/projectAware/MoveSettingsFilesService.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.move.cli.projectAware - -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.project.Project -import org.move.cli.Consts -import org.move.cli.moveProjectsService - -@Service(Service.Level.PROJECT) -class MoveSettingsFilesService(private val project: Project) { - fun collectSettingsFiles(): List { - val out = mutableListOf() - for (moveProject in project.moveProjectsService.allProjects) { - for (movePackage in moveProject.movePackages()) { - val root = movePackage.contentRoot.path - out.add("$root/${Consts.MANIFEST_FILE}") - } - } - return out - } - - companion object { - fun getInstance(project: Project): MoveSettingsFilesService = project.service() - } -} diff --git a/src/main/kotlin/org/move/cli/runConfigurations/InitProjectCli.kt b/src/main/kotlin/org/move/cli/runConfigurations/BlockchainCli.kt similarity index 64% rename from src/main/kotlin/org/move/cli/runConfigurations/InitProjectCli.kt rename to src/main/kotlin/org/move/cli/runConfigurations/BlockchainCli.kt index da722f356..5cbb67930 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/InitProjectCli.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/BlockchainCli.kt @@ -1,18 +1,21 @@ package org.move.cli.runConfigurations +import com.intellij.execution.process.ProcessListener import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import org.move.cli.Consts -import org.move.cli.settings.Blockchain -import org.move.cli.settings.aptos.AptosExec import org.move.openapiext.* import org.move.openapiext.common.isUnitTestMode -import org.move.stdext.RsResult +import org.move.stdext.RsResult.Err +import org.move.stdext.RsResult.Ok import org.move.stdext.unwrapOrElse import java.nio.file.Path -sealed class InitProjectCli { +sealed class BlockchainCli { + + abstract val cliLocation: Path + abstract fun init( project: Project, parentDisposable: Disposable, @@ -20,7 +23,7 @@ sealed class InitProjectCli { packageName: String, ): MvProcessResult - data class Aptos(val aptosExec: AptosExec): InitProjectCli() { + data class Aptos(override val cliLocation: Path): BlockchainCli() { override fun init( project: Project, parentDisposable: Disposable, @@ -39,19 +42,19 @@ sealed class InitProjectCli { ), workingDirectory = project.rootPath ) - val aptosPath = this.aptosExec.toPathOrNull() ?: error("unreachable") - commandLine.toGeneralCommandLine(aptosPath) + commandLine + .toGeneralCommandLine(cliLocation) .execute(parentDisposable) - .unwrapOrElse { return RsResult.Err(it) } + .unwrapOrElse { return Err(it) } fullyRefreshDirectory(rootDirectory) val manifest = checkNotNull(rootDirectory.findChild(Consts.MANIFEST_FILE)) { "Can't find the manifest file" } - return RsResult.Ok(manifest) + return Ok(manifest) } } - data class Sui(val cliLocation: Path): InitProjectCli() { + data class Sui(override val cliLocation: Path): BlockchainCli() { override fun init( project: Project, parentDisposable: Disposable, @@ -71,12 +74,29 @@ sealed class InitProjectCli { ) commandLine.toGeneralCommandLine(this.cliLocation) .execute(parentDisposable) - .unwrapOrElse { return RsResult.Err(it) } + .unwrapOrElse { return Err(it) } fullyRefreshDirectory(rootDirectory) val manifest = checkNotNull(rootDirectory.findChild(Consts.MANIFEST_FILE)) { "Can't find the manifest file" } - return RsResult.Ok(manifest) + return Ok(manifest) + } + + fun fetchPackageDependencies( + projectDir: Path, + owner: Disposable, + processListener: ProcessListener + ): MvProcessResult { + val cli = + CliCommandLineArgs( + subCommand = "move", + arguments = listOf("build", "--fetch-deps-only", "--skip-fetch-latest-git-deps"), + workingDirectory = projectDir + ) + cli.toGeneralCommandLine(cliLocation) + .execute(owner, listener = processListener) + .unwrapOrElse { return Err(it) } + return Ok(Unit) } } } diff --git a/src/main/kotlin/org/move/cli/runConfigurations/MvCapturingProcessHandler.kt b/src/main/kotlin/org/move/cli/runConfigurations/MvCapturingProcessHandler.kt index 9c5e0ee47..c53d26f3c 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/MvCapturingProcessHandler.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/MvCapturingProcessHandler.kt @@ -7,12 +7,13 @@ package org.move.cli.runConfigurations import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.CapturingAnsiEscapesAwareProcessHandler import com.intellij.execution.process.CapturingProcessHandler import com.intellij.util.io.BaseOutputReader import org.move.stdext.RsResult class MvCapturingProcessHandler private constructor(commandLine: GeneralCommandLine) : - CapturingProcessHandler(commandLine) { + CapturingAnsiEscapesAwareProcessHandler(commandLine) { override fun readerOptions(): BaseOutputReader.Options = BaseOutputReader.Options.BLOCKING diff --git a/src/main/kotlin/org/move/cli/runConfigurations/aptos/FunctionCallConfigurationBase.kt b/src/main/kotlin/org/move/cli/runConfigurations/aptos/FunctionCallConfigurationBase.kt index 54796a53c..1174134c1 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/aptos/FunctionCallConfigurationBase.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/aptos/FunctionCallConfigurationBase.kt @@ -5,7 +5,7 @@ import com.intellij.openapi.project.Project import org.move.cli.MoveProject import org.move.cli.moveProjectsService import org.move.cli.runConfigurations.CommandConfigurationBase -import org.move.cli.settings.aptosPath +import org.move.cli.settings.aptosExecPath import java.nio.file.Path abstract class FunctionCallConfigurationBase( @@ -20,7 +20,7 @@ abstract class FunctionCallConfigurationBase( workingDirectory = value?.contentRootPath } - override fun getCliPath(project: Project): Path? = project.aptosPath + override fun getCliPath(project: Project): Path? = project.aptosExecPath fun firstRunShouldOpenEditor(): Boolean { val moveProject = moveProjectFromWorkingDirectory ?: return true diff --git a/src/main/kotlin/org/move/cli/runConfigurations/aptos/any/AnyCommandConfiguration.kt b/src/main/kotlin/org/move/cli/runConfigurations/aptos/any/AnyCommandConfiguration.kt index 1fcaefe96..f18d5b039 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/aptos/any/AnyCommandConfiguration.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/aptos/any/AnyCommandConfiguration.kt @@ -4,7 +4,7 @@ import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.openapi.project.Project import org.move.cli.moveProjectsService import org.move.cli.runConfigurations.CommandConfigurationBase -import org.move.cli.settings.aptosPath +import org.move.cli.settings.aptosExecPath import java.nio.file.Path class AnyCommandConfiguration( @@ -21,7 +21,7 @@ class AnyCommandConfiguration( } } - override fun getCliPath(project: Project): Path? = project.aptosPath + override fun getCliPath(project: Project): Path? = project.aptosExecPath override fun getConfigurationEditor() = AnyCommandConfigurationEditor() } diff --git a/src/main/kotlin/org/move/cli/runConfigurations/legacy/MoveCommandConfiguration.kt b/src/main/kotlin/org/move/cli/runConfigurations/legacy/MoveCommandConfiguration.kt index b83d914a2..3509e3ca1 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/legacy/MoveCommandConfiguration.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/legacy/MoveCommandConfiguration.kt @@ -9,7 +9,7 @@ import com.intellij.openapi.util.NlsContexts import com.intellij.util.execution.ParametersListUtil import org.jdom.Element import org.move.cli.* -import org.move.cli.settings.aptosPath +import org.move.cli.settings.aptosExecPath import org.move.stdext.exists import java.nio.file.Path @@ -66,7 +66,7 @@ class MoveCommandConfiguration( parsed.additionalArguments, environmentVariables ) - val aptosPath = project.aptosPath + val aptosPath = project.aptosExecPath ?: return CleanConfiguration.error("No Aptos CLI specified") if (!aptosPath.exists()) { diff --git a/src/main/kotlin/org/move/cli/runConfigurations/producers/CommandConfigurationProducerBase.kt b/src/main/kotlin/org/move/cli/runConfigurations/producers/CommandConfigurationProducerBase.kt index af3819351..1a605d77a 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/producers/CommandConfigurationProducerBase.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/producers/CommandConfigurationProducerBase.kt @@ -14,7 +14,7 @@ abstract class CommandConfigurationProducerBase(val blockchain: Blockchain): fun configFromLocation(location: PsiElement, climbUp: Boolean = true): CommandLineArgsFromContext? { val project = location.project - if (project.moveSettings.state.blockchain != blockchain) { + if (project.moveSettings.blockchain != blockchain) { return null } return fromLocation(location, climbUp) @@ -36,7 +36,7 @@ abstract class CommandConfigurationProducerBase(val blockchain: Blockchain): var envVars = commandLine.environmentVariables if (blockchain == Blockchain.APTOS - && context.project.moveSettings.state.disableTelemetry + && context.project.moveSettings.disableTelemetry ) { envVars = envVars.with(mapOf("APTOS_DISABLE_TELEMETRY" to "true")) } @@ -49,7 +49,7 @@ abstract class CommandConfigurationProducerBase(val blockchain: Blockchain): context: ConfigurationContext ): Boolean { val project = context.project - if (project.moveSettings.state.blockchain != blockchain) { + if (project.moveSettings.blockchain != blockchain) { return false } val location = context.psiLocation ?: return false diff --git a/src/main/kotlin/org/move/cli/runConfigurations/producers/TestCommandConfigurationProducerBase.kt b/src/main/kotlin/org/move/cli/runConfigurations/producers/TestCommandConfigurationProducerBase.kt index 47423cd05..da81961b8 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/producers/TestCommandConfigurationProducerBase.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/producers/TestCommandConfigurationProducerBase.kt @@ -6,8 +6,7 @@ import com.intellij.psi.PsiFileSystemItem import org.move.cli.MoveProject import org.move.cli.runConfigurations.CliCommandLineArgs import org.move.cli.settings.Blockchain -import org.move.cli.settings.dumpStateOnTestFailure -import org.move.cli.settings.skipFetchLatestGitDeps +import org.move.cli.settings.moveSettings import org.move.lang.MoveFile import org.move.lang.core.psi.MvFunction import org.move.lang.core.psi.MvModule @@ -59,10 +58,10 @@ abstract class TestCommandConfigurationProducerBase(blockchain: Blockchain): Blockchain.APTOS -> subCommand += " --filter $modName::$functionName" Blockchain.SUI -> subCommand += " $modName::$functionName" } - if (psi.project.skipFetchLatestGitDeps) { + if (psi.project.moveSettings.skipFetchLatestGitDeps) { subCommand += " --skip-fetch-latest-git-deps" } - if (blockchain == Blockchain.APTOS && psi.project.dumpStateOnTestFailure) { + if (blockchain == Blockchain.APTOS && psi.project.moveSettings.dumpStateOnTestFailure) { subCommand += " --dump" } @@ -87,10 +86,10 @@ abstract class TestCommandConfigurationProducerBase(blockchain: Blockchain): Blockchain.APTOS -> subCommand += " --filter $modName" Blockchain.SUI -> subCommand += " $modName" } - if (psi.project.skipFetchLatestGitDeps) { + if (psi.project.moveSettings.skipFetchLatestGitDeps) { subCommand += " --skip-fetch-latest-git-deps" } - if (blockchain == Blockchain.APTOS && psi.project.dumpStateOnTestFailure) { + if (blockchain == Blockchain.APTOS && psi.project.moveSettings.dumpStateOnTestFailure) { subCommand += " --dump" } @@ -112,10 +111,10 @@ abstract class TestCommandConfigurationProducerBase(blockchain: Blockchain): val confName = "Test $packageName" var subCommand = "move test" - if (location.project.skipFetchLatestGitDeps) { + if (location.project.moveSettings.skipFetchLatestGitDeps) { subCommand += " --skip-fetch-latest-git-deps" } - if (location.project.dumpStateOnTestFailure) { + if (location.project.moveSettings.dumpStateOnTestFailure) { subCommand += " --dump" } diff --git a/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt b/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt index 465c159d9..90f122eb5 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.project.Project import org.move.cli.moveProjectsService import org.move.cli.runConfigurations.CommandConfigurationBase import org.move.cli.settings.moveSettings +import org.move.cli.settings.suiExecPath import org.move.stdext.toPathOrNull import java.nio.file.Path @@ -22,11 +23,7 @@ class SuiCommandConfiguration( } } - override fun getCliPath(project: Project): Path? { - return project.moveSettings.state.suiPath - .takeIf { it.isNotBlank() } - ?.toPathOrNull() - } + override fun getCliPath(project: Project): Path? = project.suiExecPath override fun getConfigurationEditor() = SuiCommandConfigurationEditor() } diff --git a/src/main/kotlin/org/move/cli/sentryReporter/SentryContexts.kt b/src/main/kotlin/org/move/cli/sentryReporter/SentryContexts.kt index 1164cc302..bbeefd7c1 100644 --- a/src/main/kotlin/org/move/cli/sentryReporter/SentryContexts.kt +++ b/src/main/kotlin/org/move/cli/sentryReporter/SentryContexts.kt @@ -20,8 +20,8 @@ data class MoveProjectContext( val syntheticLibraries: List ) { companion object { - fun from(moveProject: MoveProject): MoveProjectContext? { - val tomlFile = moveProject.currentPackage.moveToml.tomlFile ?: return null + fun from(moveProject: MoveProject): MoveProjectContext { + val tomlFile = moveProject.currentPackage.moveToml.tomlFile val rawDeps = mutableListOf() val depsTable = tomlFile.getTable("dependencies") diff --git a/src/main/kotlin/org/move/cli/settings/MoveProjectSettingsService.kt b/src/main/kotlin/org/move/cli/settings/MoveProjectSettingsService.kt deleted file mode 100644 index 6964880bf..000000000 --- a/src/main/kotlin/org/move/cli/settings/MoveProjectSettingsService.kt +++ /dev/null @@ -1,199 +0,0 @@ -package org.move.cli.settings - -import com.intellij.configurationStore.serializeObjectInto -import com.intellij.openapi.Disposable -import com.intellij.openapi.components.* -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer -import com.intellij.psi.PsiManager -import com.intellij.util.messages.Topic -import com.intellij.util.xmlb.XmlSerializer -import org.jdom.Element -import org.jetbrains.annotations.TestOnly -import org.move.cli.settings.aptos.AptosExec -import org.move.openapiext.debugInProduction -import org.move.stdext.exists -import org.move.stdext.isExecutableFile -import org.move.stdext.toPathOrNull -import java.nio.file.Path -import kotlin.reflect.KProperty1 -import kotlin.reflect.full.memberProperties - -data class MoveSettingsChangedEvent( - val oldState: MoveProjectSettingsService.State, - val newState: MoveProjectSettingsService.State, -) { - /** Use it like `event.isChanged(State::foo)` to check whether `foo` property is changed or not */ - fun isChanged(prop: KProperty1): Boolean = - prop.get(oldState) != prop.get(newState) -} - -interface MoveSettingsListener { - fun moveSettingsChanged(e: MoveSettingsChangedEvent) -} - -enum class Blockchain { - APTOS, SUI; - - override fun toString(): String = if (this == APTOS) "Aptos" else "Sui" -} - -private const val settingsServiceName: String = "MoveProjectSettingsService_1" - -@Service(Service.Level.PROJECT) -@State( - name = settingsServiceName, - storages = [ - Storage(StoragePathMacros.WORKSPACE_FILE), - Storage("misc.xml", deprecated = true) - ] -) -class MoveProjectSettingsService(private val project: Project): PersistentStateComponent { - - // default values for settings - data class State( - // null not Mac -> Bundled, null and Mac -> Local(""), not null -> Local(value) - var blockchain: Blockchain = Blockchain.APTOS, - var aptosPath: String? = if (AptosExec.isBundledSupportedForThePlatform()) null else "", - var suiPath: String = "", - var foldSpecs: Boolean = false, - var disableTelemetry: Boolean = true, - var debugMode: Boolean = false, - var skipFetchLatestGitDeps: Boolean = false, - var dumpStateOnTestFailure: Boolean = false, - ) { - fun aptosExec(): AptosExec { - val path = aptosPath - return when (path) { - null -> AptosExec.Bundled - else -> AptosExec.LocalPath(path) - } - } - } - - @Volatile - private var _state = State() - - var state: State - get() = _state.copy() - set(newState) { - if (_state != newState) { - val oldState = _state - _state = newState.copy() - notifySettingsChanged(oldState, newState) - } - } - - private fun notifySettingsChanged( - oldState: State, - newState: State, - ) { - val event = MoveSettingsChangedEvent(oldState, newState) - - for (prop in State::class.memberProperties) { - if (event.isChanged(prop)) { - val oldValue = prop.get(oldState) - val newValue = prop.get(newState) - LOG.debugInProduction("SETTINGS updated [${prop.name}: $oldValue -> $newValue]") - } - } - - project.messageBus.syncPublisher(MOVE_SETTINGS_TOPIC).moveSettingsChanged(event) - - if (event.isChanged(State::foldSpecs)) { - PsiManager.getInstance(project).dropPsiCaches() - } - } - - override fun getState(): Element { - val element = Element(settingsServiceName) - serializeObjectInto(_state, element) - return element - } - - override fun loadState(element: Element) { - val rawState = element.clone() - XmlSerializer.deserializeInto(_state, rawState) - } - - /** - * Allows to modify settings. - * After setting change, - */ - fun modify(action: (State) -> Unit) { - val oldState = state.copy() - val newState = state.copy().also(action) - state = newState - - notifySettingsChanged(oldState, newState) -// val event = MoveSettingsChangedEvent(oldState, newState) -// project.messageBus.syncPublisher(MOVE_SETTINGS_TOPIC).moveSettingsChanged(event) - } - - /** - * Allows to modify settings. - * After setting change, - */ - @TestOnly - fun modifyTemporary(parentDisposable: Disposable, action: (State) -> Unit) { - val oldState = state - state = oldState.also(action) - Disposer.register(parentDisposable) { - _state = oldState - } - } - - /** - * Returns current state of the service. - * Note, result is a copy of service state, so you need to set modified state back to apply changes - */ - companion object { - val MOVE_SETTINGS_TOPIC = Topic("move settings changes", MoveSettingsListener::class.java) - - private val LOG = logger() - } -} - -val Project.moveSettings: MoveProjectSettingsService get() = service() - -val Project.collapseSpecs: Boolean get() = this.moveSettings.state.foldSpecs - -val Project.blockchain: Blockchain get() = this.moveSettings.state.blockchain - -val Project.aptosExec: AptosExec get() = this.moveSettings.state.aptosExec() - -val Project.aptosPath: Path? get() = this.aptosExec.toPathOrNull() - -val Project.suiPath: Path? get() = this.moveSettings.state.suiPath.toPathOrNull() - -fun Path?.isValidExecutable(): Boolean { - return this != null - && this.toString().isNotBlank() - && this.exists() - && this.isExecutableFile() -} - -val Project.isDebugModeEnabled: Boolean get() = this.moveSettings.state.debugMode - -fun Project.debugErrorOrFallback(message: String, fallback: T): T { - if (this.isDebugModeEnabled) { - error(message) - } - return fallback -} - -fun Project.debugErrorOrFallback(message: String, cause: Throwable?, fallback: () -> T): T { - if (this.isDebugModeEnabled) { - throw IllegalStateException(message, cause) - } - return fallback() -} - -val Project.skipFetchLatestGitDeps: Boolean - get() = - this.moveSettings.state.skipFetchLatestGitDeps - -val Project.dumpStateOnTestFailure: Boolean - get() = - this.moveSettings.state.dumpStateOnTestFailure diff --git a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt new file mode 100644 index 000000000..ccf8df557 --- /dev/null +++ b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt @@ -0,0 +1,161 @@ +package org.move.cli.settings + +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.StoragePathMacros +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiManager +import org.move.cli.runConfigurations.BlockchainCli +import org.move.cli.runConfigurations.BlockchainCli.Aptos +import org.move.cli.runConfigurations.BlockchainCli.Sui +import org.move.cli.settings.Blockchain.APTOS +import org.move.cli.settings.Blockchain.SUI +import org.move.cli.settings.MvProjectSettingsService.MoveProjectSettings +import org.move.cli.settings.aptos.AptosExecType +import org.move.stdext.exists +import org.move.stdext.isExecutableFile +import org.move.stdext.toPathOrNull +import org.move.utils.EnvUtils +import java.nio.file.Path + +enum class Blockchain { + APTOS, SUI; + + override fun toString(): String = if (this == APTOS) "Aptos" else "Sui" + + companion object { + fun aptosFromPATH(): String? { + // TODO: run --version and check whether it's a real Aptos CLI executable + return EnvUtils.findInPATH("aptos")?.toAbsolutePath()?.toString() + } + + fun suiFromPATH(): String? { + // TODO: same as in Aptos + return EnvUtils.findInPATH("sui")?.toAbsolutePath()?.toString() + } + } +} + +val Project.moveSettings: MvProjectSettingsService get() = service() + +private const val SERVICE_NAME: String = "MoveProjectSettingsService_1" + +@State( + name = SERVICE_NAME, + storages = [Storage(StoragePathMacros.WORKSPACE_FILE)] +) +class MvProjectSettingsService( + project: Project +): + MvProjectSettingsServiceBase(project, MoveProjectSettings()) { + + val blockchain: Blockchain get() = state.blockchain + + val aptosExecType: AptosExecType get() = state.aptosExecType + val localAptosPath: String? get() = state.localAptosPath + val localSuiPath: String? get() = state.localSuiPath + + val disableTelemetry: Boolean get() = state.disableTelemetry + val foldSpecs: Boolean get() = state.foldSpecs + val skipFetchLatestGitDeps: Boolean get() = state.skipFetchLatestGitDeps + val dumpStateOnTestFailure: Boolean get() = state.dumpStateOnTestFailure + + // default values for settings + class MoveProjectSettings: MvProjectSettingsBase() { + @AffectsMoveProjectsMetadata + var blockchain: Blockchain by enum(Blockchain.APTOS) + + @AffectsMoveProjectsMetadata + var aptosExecType: AptosExecType by enum(defaultAptosExecType) + + @AffectsMoveProjectsMetadata + var localAptosPath: String? by string() + + @AffectsMoveProjectsMetadata + var localSuiPath: String? by string() + + var foldSpecs: Boolean by property(false) + var disableTelemetry: Boolean by property(true) + var debugMode: Boolean by property(false) + var skipFetchLatestGitDeps: Boolean by property(false) + var dumpStateOnTestFailure: Boolean by property(false) + + override fun copy(): MoveProjectSettings { + val state = MoveProjectSettings() + state.copyFrom(this) + return state + } + } + + override fun notifySettingsChanged(event: SettingsChangedEventBase) { + super.notifySettingsChanged(event) + + if (event.isChanged(MoveProjectSettings::foldSpecs)) { + PsiManager.getInstance(project).dropPsiCaches() + } + } + + override fun createSettingsChangedEvent( + oldEvent: MoveProjectSettings, + newEvent: MoveProjectSettings + ): SettingsChangedEvent = SettingsChangedEvent(oldEvent, newEvent) + + class SettingsChangedEvent( + oldState: MoveProjectSettings, + newState: MoveProjectSettings + ): SettingsChangedEventBase(oldState, newState) + + companion object { + private val defaultAptosExecType + get() = + if (AptosExecType.isBundledSupportedForThePlatform) AptosExecType.BUNDLED else AptosExecType.LOCAL; + } +} + +val Project.blockchain: Blockchain get() = this.moveSettings.blockchain + +fun Project.getBlockchainCli(blockchain: Blockchain): BlockchainCli? { + return when (blockchain) { + APTOS -> { + val aptosExecPath = + AptosExecType.aptosExecPath( + this.moveSettings.aptosExecType, + this.moveSettings.localAptosPath + ) + aptosExecPath?.let { Aptos(it) } + } + SUI -> this.moveSettings.localSuiPath?.toPathOrNull()?.let { Sui(it) } + } +} + +val Project.aptosCli: Aptos? get() = getBlockchainCli(APTOS) as? Aptos + +val Project.suiCli: Sui? get() = getBlockchainCli(SUI) as? Sui + +val Project.aptosExecPath: Path? get() = this.aptosCli?.cliLocation + +val Project.suiExecPath: Path? get() = this.suiCli?.cliLocation + +fun Path?.isValidExecutable(): Boolean { + return this != null + && this.toString().isNotBlank() + && this.exists() + && this.isExecutableFile() +} + +val Project.isDebugModeEnabled: Boolean get() = this.moveSettings.state.debugMode + +fun Project.debugErrorOrFallback(message: String, fallback: T): T { + if (this.isDebugModeEnabled) { + error(message) + } + return fallback +} + +fun Project.debugErrorOrFallback(message: String, cause: Throwable?, fallback: () -> T): T { + if (this.isDebugModeEnabled) { + throw IllegalStateException(message, cause) + } + return fallback() +} diff --git a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsServiceBase.kt b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsServiceBase.kt new file mode 100644 index 000000000..3995cf2b8 --- /dev/null +++ b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsServiceBase.kt @@ -0,0 +1,89 @@ +package org.move.cli.settings + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.BaseState +import com.intellij.openapi.components.SimplePersistentStateComponent +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.util.messages.Topic +import org.jetbrains.annotations.TestOnly +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.memberProperties +import org.move.cli.settings.MvProjectSettingsServiceBase.MvProjectSettingsBase + +abstract class MvProjectSettingsServiceBase>( + val project: Project, + state: T +): SimplePersistentStateComponent(state) { + + abstract class MvProjectSettingsBase>: BaseState() { + abstract fun copy(): T + } + + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.PROPERTY) + protected annotation class AffectsMoveProjectsMetadata + + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.PROPERTY) + protected annotation class AffectsHighlighting + + fun modify(modifySettings: (T) -> Unit) { + val oldState = this.state.copy() + // assigns new values to `this.state` + val newState = this.state.also(modifySettings) + + val event = createSettingsChangedEvent(oldState, newState) + notifySettingsChanged(event) + } + + @TestOnly + fun modifyTemporary(parentDisposable: Disposable, modifySettings: (T) -> Unit) { + val oldState = state + loadState(oldState.copy().also(modifySettings)) + Disposer.register(parentDisposable) { + loadState(oldState) + } + } + + companion object { + val MOVE_SETTINGS_TOPIC: Topic = Topic.create( + "move settings changes", + MoveSettingsListener::class.java, +// Topic.BroadcastDirection.TO_PARENT + ) + } + + interface MoveSettingsListener { + fun > settingsChanged(e: SettingsChangedEventBase) + } + + protected abstract fun createSettingsChangedEvent(oldEvent: T, newEvent: T): SettingsChangedEventBase + + protected open fun notifySettingsChanged(event: SettingsChangedEventBase) { + project.messageBus.syncPublisher(MOVE_SETTINGS_TOPIC).settingsChanged(event) + + if (event.affectsHighlighting) { + DaemonCodeAnalyzer.getInstance(project).restart() + } + } + + abstract class SettingsChangedEventBase>(val oldState: T, val newState: T) { + private val moveProjectsMetadataAffectingProps: List> = + oldState.javaClass.kotlin.memberProperties.filter { it.findAnnotation() != null } + + private val highlightingAffectingProps: List> = + oldState.javaClass.kotlin.memberProperties.filter { it.findAnnotation() != null } + + val affectsMoveProjectsMetadata: Boolean + get() = moveProjectsMetadataAffectingProps.any(::isChanged) + + val affectsHighlighting: Boolean + get() = highlightingAffectingProps.any(::isChanged) + + /** Use it like `event.isChanged(State::foo)` to check whether `foo` property is changed or not */ + fun isChanged(prop: KProperty1): Boolean = prop.get(oldState) != prop.get(newState) + } +} diff --git a/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt b/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt index 9e524039f..a75005327 100644 --- a/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt +++ b/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt @@ -9,7 +9,7 @@ import com.intellij.ui.components.JBRadioButton import com.intellij.ui.dsl.builder.* import org.move.cli.settings.aptos.ChooseAptosCliPanel import org.move.cli.settings.sui.ChooseSuiCliPanel -import org.move.openapiext.showSettings +import org.move.openapiext.showSettingsDialog class PerProjectMoveConfigurable(val project: Project): BoundSearchableConfigurable( @@ -18,111 +18,121 @@ class PerProjectMoveConfigurable(val project: Project): _id = "org.move.settings" ) { - private val settingsState: MoveProjectSettingsService.State = project.moveSettings.state - private val chooseAptosCliPanel = ChooseAptosCliPanel(versionUpdateListener = null) private val chooseSuiCliPanel = ChooseSuiCliPanel() override fun createPanel(): DialogPanel { - return panel { - group { - var aptosRadioButton: Cell? = null - var suiRadioButton: Cell? = null - buttonsGroup("Blockchain") { - row { - aptosRadioButton = radioButton("Aptos") - .bindSelected( - { settingsState.blockchain == Blockchain.APTOS }, - { settingsState.blockchain = Blockchain.APTOS }, - ) - suiRadioButton = radioButton("Sui") - .bindSelected( - { settingsState.blockchain == Blockchain.SUI }, - { settingsState.blockchain = Blockchain.SUI }, - ) + val configurablePanel = + panel { + val settings = project.moveSettings + val state = settings.state.copy() + + group { + var aptosRadioButton: Cell? = null + var suiRadioButton: Cell? = null + buttonsGroup("Blockchain") { + row { + aptosRadioButton = radioButton("Aptos") + .bindSelected( + { state.blockchain == Blockchain.APTOS }, + { state.blockchain = Blockchain.APTOS }, + ) + suiRadioButton = radioButton("Sui") + .bindSelected( + { state.blockchain == Blockchain.SUI }, + { state.blockchain = Blockchain.SUI }, + ) + } } + chooseAptosCliPanel.attachToLayout(this) + .visibleIf(aptosRadioButton!!.selected) + chooseSuiCliPanel.attachToLayout(this) + .visibleIf(suiRadioButton!!.selected) } - chooseAptosCliPanel.attachToLayout(this) - .visibleIf(aptosRadioButton!!.selected) - chooseSuiCliPanel.attachToLayout(this) - .visibleIf(suiRadioButton!!.selected) - } - group { - row { - checkBox("Auto-fold specs in opened files") - .bindSelected(settingsState::foldSpecs) - } - row { - checkBox("Disable telemetry for new Run Configurations") - .bindSelected(settingsState::disableTelemetry) - } - row { - checkBox("Enable debug mode") - .bindSelected(settingsState::debugMode) - comment( - "Enables some explicit crashes in the plugin code. Useful for the error reporting." - ) - } - row { - checkBox("Skip fetching latest git dependencies for tests") - .bindSelected(settingsState::skipFetchLatestGitDeps) - comment( - "Adds --skip-fetch-latest-git-deps to the test runs." - ) - } - row { - checkBox("Dump storage to console on test failures") - .bindSelected(settingsState::dumpStateOnTestFailure) - comment( - "Adds --dump to the test runs (aptos only)." - ) + group { + row { + checkBox("Auto-fold specs in opened files") + .bindSelected(state::foldSpecs) + } + row { + checkBox("Disable telemetry for new Run Configurations") + .bindSelected(state::disableTelemetry) + } + row { + checkBox("Enable debug mode") + .bindSelected(state::debugMode) + comment( + "Enables some explicit crashes in the plugin code. Useful for the error reporting." + ) + } + row { + checkBox("Skip fetching latest git dependencies for tests") + .bindSelected(state::skipFetchLatestGitDeps) + comment( + "Adds --skip-fetch-latest-git-deps to the test runs." + ) + } + row { + checkBox("Dump storage to console on test failures") + .bindSelected(state::dumpStateOnTestFailure) + comment( + "Adds --dump to the test runs (aptos only)." + ) + } } - } - if (!project.isDefault) { - row { - link("Set default project settings") { - ProjectManager.getInstance().defaultProject.showSettings() + + if (!project.isDefault) { + row { + link("Set default project settings") { + ProjectManager.getInstance().defaultProject.showSettingsDialog() + } + // .visible(true) + .align(AlignX.RIGHT) } -// .visible(true) - .align(AlignX.RIGHT) } - } - } - } - override fun disposeUIResources() { - super.disposeUIResources() - Disposer.dispose(chooseAptosCliPanel) - Disposer.dispose(chooseSuiCliPanel) - } + // saves values from Swing form back to configurable (OK / Apply) + onApply { + settings.modify { + it.aptosExecType = chooseAptosCliPanel.data.aptosExecType + it.localAptosPath = chooseAptosCliPanel.data.localAptosPath - /// checks whether any settings are modified (should be fast) - override fun isModified(): Boolean { - // checks whether panel created in the createPanel() is modified, defined in DslConfigurableBase - if (super.isModified()) return true - val selectedAptosExec = chooseAptosCliPanel.selectedAptosExec - val selectedSui = chooseSuiCliPanel.getSuiCliPath() - return selectedAptosExec != settingsState.aptosExec() - || selectedSui != settingsState.suiPath - } + it.localSuiPath = chooseSuiCliPanel.data.localSuiPath - /// loads settings from configurable to swing form - override fun reset() { - chooseAptosCliPanel.selectedAptosExec = settingsState.aptosExec() - chooseSuiCliPanel.setSuiCliPath(settingsState.suiPath) - // resets panel created in createPanel(), see DslConfigurableBase - // should be invoked at the end - super.reset() - } + it.blockchain = state.blockchain + it.foldSpecs = state.foldSpecs + it.disableTelemetry = state.disableTelemetry + it.debugMode = state.debugMode + it.skipFetchLatestGitDeps = state.skipFetchLatestGitDeps + it.dumpStateOnTestFailure = state.dumpStateOnTestFailure + } + } + + // loads settings from configurable to swing form + onReset { + chooseAptosCliPanel.data = + ChooseAptosCliPanel.Data(state.aptosExecType, state.localAptosPath) + chooseSuiCliPanel.data = + ChooseSuiCliPanel.Data(state.localSuiPath) + } - /// saves values from Swing form back to configurable (OK / Apply) - override fun apply() { - // calls apply() for createPanel().value - super.apply() - project.moveSettings.state = - settingsState.copy( - aptosPath = chooseAptosCliPanel.selectedAptosExec.pathToSettingsFormat(), - suiPath = chooseSuiCliPanel.getSuiCliPath() - ) + /// checks whether any settings are modified (should be fast) + onIsModified { + val aptosPanelData = chooseAptosCliPanel.data + val suiPanelData = chooseSuiCliPanel.data + aptosPanelData.aptosExecType != settings.aptosExecType + || aptosPanelData.localAptosPath != settings.localAptosPath + || suiPanelData.localSuiPath != settings.localSuiPath + } + } + Disposer.register(this.disposable!!, chooseAptosCliPanel) + Disposer.register(this.disposable!!, chooseSuiCliPanel) + return configurablePanel } + +// override fun disposeUIResources() { +// super.disposeUIResources() +// Disposer.dispose(chooseAptosCliPanel) +// Disposer.dispose(chooseSuiCliPanel) +// } } diff --git a/src/main/kotlin/org/move/cli/settings/VersionLabel.kt b/src/main/kotlin/org/move/cli/settings/VersionLabel.kt index a55f8e7e3..af12d2cd3 100644 --- a/src/main/kotlin/org/move/cli/settings/VersionLabel.kt +++ b/src/main/kotlin/org/move/cli/settings/VersionLabel.kt @@ -2,6 +2,7 @@ package org.move.cli.settings import com.intellij.openapi.Disposable import com.intellij.ui.JBColor +import com.intellij.ui.components.JBLabel import org.move.cli.runConfigurations.CliCommandLineArgs import org.move.openapiext.UiDebouncer import org.move.openapiext.checkIsBackgroundThread @@ -9,22 +10,38 @@ import org.move.openapiext.common.isUnitTestMode import org.move.openapiext.execute import org.move.openapiext.isSuccess import java.nio.file.Path +import javax.swing.Icon import javax.swing.JLabel +open class TextOrErrorLabel(icon: Icon?): JBLabel(icon) { + fun setText(text: String, errorHighlighting: Boolean) { + if (errorHighlighting) { + this.text = text + this.foreground = JBColor.RED + } else { + this.text = text + .split("\n") + .joinToString("
", "", "") + this.foreground = JBColor.foreground() + } + } +} + class VersionLabel( parentDisposable: Disposable, private val versionUpdateListener: (() -> Unit)? = null -) : JLabel() { +): + TextOrErrorLabel(null) { private val versionUpdateDebouncer = UiDebouncer(parentDisposable) - fun updateAndNotifyListeners(execPath: Path) { + fun updateAndNotifyListeners(execPath: Path?) { versionUpdateDebouncer.update( onPooledThread = { if (!isUnitTestMode) { checkIsBackgroundThread() } - if (!execPath.isValidExecutable()) { + if (execPath == null || !execPath.isValidExecutable()) { return@update null } @@ -56,15 +73,15 @@ class VersionLabel( fun setTextInvalidExecutable() = this.setText("N/A (Invalid executable)", errorHighlighting = true) - fun setText(text: String, errorHighlighting: Boolean) { - if (errorHighlighting) { - this.text = text - this.foreground = JBColor.RED - } else { - this.text = text - .split("\n") - .joinToString("
", "", "") - this.foreground = JBColor.foreground() - } - } +// fun setText(text: String, errorHighlighting: Boolean) { +// if (errorHighlighting) { +// this.text = text +// this.foreground = JBColor.RED +// } else { +// this.text = text +// .split("\n") +// .joinToString("
", "", "") +// this.foreground = JBColor.foreground() +// } +// } } diff --git a/src/main/kotlin/org/move/cli/settings/aptos/AptosExec.kt b/src/main/kotlin/org/move/cli/settings/aptos/AptosExec.kt deleted file mode 100644 index 5f0e2ebc8..000000000 --- a/src/main/kotlin/org/move/cli/settings/aptos/AptosExec.kt +++ /dev/null @@ -1,48 +0,0 @@ -package org.move.cli.settings.aptos - -import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.util.SystemInfo -import org.move.cli.settings.MoveProjectSettingsService -import org.move.cli.settings.isValidExecutable -import org.move.openapiext.PluginPathManager -import org.move.stdext.toPathOrNull - -sealed class AptosExec { - abstract val execPath: String - - object Bundled: AptosExec() { - override val execPath: String - get() = PluginPathManager.bundledAptosCli ?: "" - } - - data class LocalPath(override val execPath: String): AptosExec() - - fun isValid(): Boolean { - if (this is Bundled && !isBundledSupportedForThePlatform()) return false - return this.toPathOrNull().isValidExecutable() - } - - fun toPathOrNull() = this.execPath.toPathOrNull() - - fun pathToSettingsFormat(): String? = - when (this) { - is LocalPath -> this.execPath - is Bundled -> null - } - - companion object { - fun default(): AptosExec { - // Don't use `Project.moveSettings` here because `getService` can return `null` - // for default project after dynamic plugin loading. As a result, you can get - // `java.lang.IllegalStateException`. So let's handle it manually: - val defaultProjectSettings = - ProjectManager.getInstance().defaultProject.getService(MoveProjectSettingsService::class.java) - - val defaultProjectAptosExec = defaultProjectSettings?.state?.aptosExec() - return defaultProjectAptosExec - ?: if (isBundledSupportedForThePlatform()) Bundled else LocalPath("") - } - - fun isBundledSupportedForThePlatform(): Boolean = !SystemInfo.isMac - } -} diff --git a/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt b/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt index 26f6ee9f3..1a0831edd 100644 --- a/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt +++ b/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt @@ -3,110 +3,130 @@ package org.move.cli.settings.aptos import com.intellij.openapi.Disposable import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.util.Disposer -import com.intellij.ui.dsl.builder.* +import com.intellij.openapi.util.SystemInfo +import com.intellij.ui.components.JBRadioButton +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.Row +import com.intellij.ui.dsl.builder.actionListener +import com.intellij.ui.layout.selected import org.move.cli.settings.VersionLabel +import org.move.cli.settings.aptos.AptosExecType.BUNDLED +import org.move.cli.settings.aptos.AptosExecType.LOCAL +import org.move.cli.settings.isValidExecutable +import org.move.openapiext.PluginPathManager import org.move.openapiext.pathField +import org.move.stdext.blankToNull import org.move.stdext.toPathOrNull +import java.nio.file.Path + +enum class AptosExecType { + BUNDLED, + LOCAL; + + companion object { + val isBundledSupportedForThePlatform: Boolean get() = !SystemInfo.isMac +// val isBundledSupportedForThePlatform: Boolean get() = false + + fun bundledPath(): String? = PluginPathManager.bundledAptosCli + + fun aptosExecPath(execType: AptosExecType, localAptosPath: String?): Path? { + val pathCandidate = + when (execType) { + BUNDLED -> bundledPath()?.toPathOrNull() + LOCAL -> localAptosPath?.blankToNull()?.toPathOrNull() + } + return pathCandidate?.takeIf { it.isValidExecutable() } + } + } +} class ChooseAptosCliPanel(versionUpdateListener: (() -> Unit)?): Disposable { + data class Data( + val aptosExecType: AptosExecType, + val localAptosPath: String? + ) + + var data: Data + get() { + val execType = if (bundledRadioButton.isSelected) BUNDLED else LOCAL + val path = localPathField.text.blankToNull() + return Data( + aptosExecType = execType, + localAptosPath = path + ) + } + set(value) { + when (value.aptosExecType) { + BUNDLED -> { + bundledRadioButton.isSelected = true + localRadioButton.isSelected = false + } + LOCAL -> { + bundledRadioButton.isSelected = false + localRadioButton.isSelected = true + } + } + localPathField.text = value.localAptosPath ?: "" + updateVersion() + } + private val localPathField = pathField( FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor(), this, "Choose Aptos CLI", - onTextChanged = { text -> - val exec = AptosExec.LocalPath(text) - _aptosExec = exec - exec.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } + onTextChanged = { _ -> + updateVersion() }) - private val versionLabel = VersionLabel(this, versionUpdateListener) - private lateinit var _aptosExec: AptosExec - - var selectedAptosExec: AptosExec - get() = _aptosExec - set(aptosExec) { - this._aptosExec = aptosExec - when (_aptosExec) { - is AptosExec.Bundled -> localPathField.text = "" - else -> - localPathField.text = aptosExec.execPath - } - aptosExec.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } - } + private val bundledRadioButton = JBRadioButton("Bundled") + private val localRadioButton = JBRadioButton("Local") fun attachToLayout(layout: Panel): Row { - val panel = this - if (!panel::_aptosExec.isInitialized) { - panel._aptosExec = AptosExec.default() - } val resultRow = with(layout) { group("Aptos CLI") { buttonsGroup { row { - radioButton("Bundled", AptosExec.Bundled) - .bindSelected( - { _aptosExec is AptosExec.Bundled }, - { - _aptosExec = AptosExec.Bundled - val bundledPath = AptosExec.Bundled.toPathOrNull() - if (bundledPath != null) { - versionLabel.updateAndNotifyListeners(bundledPath) - } - } - ) -// .actionListener { _, _ -> -// _aptosExec = AptosExec.Bundled -// AptosExec.Bundled.toPathOrNull() -// ?.let { versionLabel.updateValueWithListener(it) } -// } - .enabled(AptosExec.isBundledSupportedForThePlatform()) + cell(bundledRadioButton) + .enabled(AptosExecType.isBundledSupportedForThePlatform) + .actionListener { _, _ -> + updateVersion() + } comment( "Bundled version is not available for this platform (refer to the official Aptos docs for more)" ) - .visible(!AptosExec.isBundledSupportedForThePlatform()) + .visible(!AptosExecType.isBundledSupportedForThePlatform) } row { - val button = radioButton("Local", AptosExec.LocalPath("")) - .bindSelected( - { _aptosExec is AptosExec.LocalPath }, - { - _aptosExec = AptosExec.LocalPath(localPathField.text) - val localPath = localPathField.text.toPathOrNull() - if (localPath != null) { - versionLabel.updateAndNotifyListeners(localPath) - } - } - ) -// .actionListener { _, _ -> -// _aptosExec = AptosExec.LocalPath(localPathField.text) -// localPathField.text.toPathOrNull() -// ?.let { versionLabel.updateAndNotifyListeners(it) } -// } + cell(localRadioButton) + .actionListener { _, _ -> + updateVersion() + } cell(localPathField) - .enabledIf(button.selected) - .align(AlignX.FILL).resizableColumn() + .enabledIf(localRadioButton.selected) + .align(AlignX.FILL) + .resizableColumn() } row("--version :") { cell(versionLabel) } } - .bind( - { _aptosExec }, - { - _aptosExec = - when (it) { - is AptosExec.Bundled -> it - is AptosExec.LocalPath -> AptosExec.LocalPath(localPathField.text) - } - } - ) } } - _aptosExec.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } + updateVersion() return resultRow } + private fun updateVersion() { + val aptosPath = + when { + bundledRadioButton.isSelected -> AptosExecType.bundledPath() + else -> localPathField.text + }?.toPathOrNull() + versionLabel.updateAndNotifyListeners(aptosPath) + } + override fun dispose() { Disposer.dispose(localPathField) } diff --git a/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt b/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt index 250254c81..da8e0dfc7 100644 --- a/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt +++ b/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt @@ -2,65 +2,59 @@ package org.move.cli.settings.sui import com.intellij.openapi.Disposable import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.util.Disposer -import com.intellij.ui.dsl.builder.* -import org.move.cli.settings.MoveProjectSettingsService +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.Row import org.move.cli.settings.VersionLabel -import org.move.openapiext.* +import org.move.openapiext.pathField import org.move.stdext.toPathOrNull -class ChooseSuiCliPanel( - private val versionUpdateListener: (() -> Unit)? = null -): Disposable { +class ChooseSuiCliPanel(versionUpdateListener: (() -> Unit)? = null): Disposable { + + data class Data( + val localSuiPath: String?, + ) + + var data: Data + get() { + return Data(localSuiPath = localPathField.text) + } + set(value) { + localPathField.text = value.localSuiPath ?: "" + updateVersion() + } private val localPathField = pathField( FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor(), this, "Choose Sui CLI", - onTextChanged = { text -> - _suiCliPath = text - _suiCliPath.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } + onTextChanged = { _ -> + updateVersion() }) private val versionLabel = VersionLabel(this, versionUpdateListener) - private lateinit var _suiCliPath: String - - fun getSuiCliPath(): String { return _suiCliPath } - fun setSuiCliPath(path: String) { - this._suiCliPath = path - localPathField.text = path - path.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } - } - fun attachToLayout(layout: Panel): Row { - val panel = this - if (!panel::_suiCliPath.isInitialized) { - val defaultProjectSettings = - ProjectManager.getInstance().defaultProject.getService(MoveProjectSettingsService::class.java) - panel._suiCliPath = defaultProjectSettings.state.suiPath - } val resultRow = with(layout) { group("Sui CLI") { row { cell(localPathField) - .bindText( - { _suiCliPath }, - { _suiCliPath = it } - ) - .onChanged { - localPathField.text.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } - } - .align(AlignX.FILL).resizableColumn() + .align(AlignX.FILL) + .resizableColumn() } row("--version :") { cell(versionLabel) } } } - _suiCliPath.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } + updateVersion() return resultRow } + private fun updateVersion() { + val localSuiPath = localPathField.text.toPathOrNull() + versionLabel.updateAndNotifyListeners(localSuiPath) + } + override fun dispose() { Disposer.dispose(localPathField) } diff --git a/src/main/kotlin/org/move/cli/toolwindow/AptosToolWindow.kt b/src/main/kotlin/org/move/cli/toolwindow/AptosToolWindow.kt index 953273d1b..0e98f4eb5 100644 --- a/src/main/kotlin/org/move/cli/toolwindow/AptosToolWindow.kt +++ b/src/main/kotlin/org/move/cli/toolwindow/AptosToolWindow.kt @@ -4,8 +4,6 @@ import com.intellij.ide.DefaultTreeExpander import com.intellij.ide.TreeExpander import com.intellij.openapi.actionSystem.* import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.ui.SimpleToolWindowPanel @@ -20,36 +18,22 @@ import org.move.cli.hasMoveProject import org.move.cli.moveProjectsService import javax.swing.JComponent -class AptosToolWindowFactory : ToolWindowFactory, DumbAware { - private val lock: Any = Any() - +class AptosToolWindowFactory: ToolWindowFactory, DumbAware { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - project.moveProjectsService.scheduleProjectsRefresh("Aptos Tool Window opened") + project.moveProjectsService + .scheduleProjectsRefresh("Aptos Tool Window opened") + val toolwindowPanel = AptosToolWindowPanel(project) val tab = ContentFactory.getInstance() .createContent(toolwindowPanel, "", false) toolWindow.contentManager.addContent(tab) } -// override fun isApplicable(project: Project): Boolean { -// if (MoveToolWindow.isRegistered(project)) return false -// -//// val cargoProjects = project.moveProjects -//// if (!cargoProjects.hasAtLeastOneValidProject -//// && cargoProjects.suggestManifests().none() -//// ) return false -// -//// synchronized(lock) { -//// val res = project.getUserData(CARGO_TOOL_WINDOW_APPLICABLE) ?: true -//// if (res) { -//// project.putUserData(CARGO_TOOL_WINDOW_APPLICABLE, false) -//// } -//// return res -//// } -// } + // TODO: isApplicable() and initializeToolWindow() cannot be copied from intellij-rust in 241, + // implement it instead with ExternalToolWindowManager later } -private class AptosToolWindowPanel(project: Project) : SimpleToolWindowPanel(true, false) { +private class AptosToolWindowPanel(project: Project): SimpleToolWindowPanel(true, false) { private val aptosTab = AptosToolWindow(project) init { @@ -80,7 +64,7 @@ class AptosToolWindow(private val project: Project) { private val projectTree = MoveProjectsTree() private val projectStructure = MoveProjectsTreeStructure(projectTree, project) - val treeExpander: TreeExpander = object : DefaultTreeExpander(projectTree) { + val treeExpander: TreeExpander = object: DefaultTreeExpander(projectTree) { override fun isCollapseAllVisible(): Boolean = project.hasMoveProject override fun isExpandAllVisible(): Boolean = project.hasMoveProject } @@ -93,42 +77,19 @@ class AptosToolWindow(private val project: Project) { with(project.messageBus.connect()) { subscribe(MoveProjectsService.MOVE_PROJECTS_TOPIC, MoveProjectsListener { _, projects -> invokeLater { - projectStructure.reloadTreeModelAsync(projects.toList()) + projectStructure.updateMoveProjects(projects.toList()) } }) } invokeLater { - val moveProjects = project.moveProjectsService.allProjects.toList() - projectStructure.reloadTreeModelAsync(moveProjects) + projectStructure.updateMoveProjects(project.moveProjectsService.allProjects.toList()) } } companion object { - private val LOG: Logger = logger() - @JvmStatic val SELECTED_MOVE_PROJECT: DataKey = DataKey.create("SELECTED_MOVE_PROJECT") const val APTOS_TOOLBAR_PLACE: String = "Aptos Toolbar" - -// private const val ID: String = "Aptos" - -// fun initializeToolWindow(project: Project) { -// try { -// val manager = ToolWindowManager.getInstance(project) as? ToolWindowManagerEx ?: return -// val bean = ToolWindowEP.EP_NAME.extensionList.find { it.id == ID } -// if (bean != null) { -// @Suppress("DEPRECATION") -// manager.initToolWindow(bean) -// } -// } catch (e: Exception) { -// LOG.error("Unable to initialize $ID tool window", e) -// } -// } - -// fun isRegistered(project: Project): Boolean { -// val manager = ToolWindowManager.getInstance(project) -// return manager.getToolWindow(ID) != null -// } } } diff --git a/src/main/kotlin/org/move/cli/toolwindow/MoveProjectsTreeStructure.kt b/src/main/kotlin/org/move/cli/toolwindow/MoveProjectsTreeStructure.kt index ea6930601..de661b1fd 100644 --- a/src/main/kotlin/org/move/cli/toolwindow/MoveProjectsTreeStructure.kt +++ b/src/main/kotlin/org/move/cli/toolwindow/MoveProjectsTreeStructure.kt @@ -34,10 +34,10 @@ class MoveProjectsTreeStructure( override fun getRootElement() = root - fun reloadTreeModelAsync(moveProjects: List): CompletableFuture<*> { + fun updateMoveProjects(moveProjects: List) { this.moveProjects = moveProjects this.root = MoveSimpleNode.Root(moveProjects) - return treeModel.invalidateAsync() + treeModel.invalidate() } sealed class MoveSimpleNode(parent: SimpleNode?) : CachingSimpleNode(parent) { diff --git a/src/main/kotlin/org/move/ide/actions/DownloadAptosAction.kt b/src/main/kotlin/org/move/ide/actions/DownloadAptosAction.kt new file mode 100644 index 000000000..524d9093a --- /dev/null +++ b/src/main/kotlin/org/move/ide/actions/DownloadAptosAction.kt @@ -0,0 +1,97 @@ +package org.move.ide.actions + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.CollectionComboBoxModel +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.ui.UIUtil +import org.move.cli.download.AptosInstaller +import org.move.cli.download.AptosItem +import org.move.cli.settings.TextOrErrorLabel +import org.move.cli.settings.isValidExecutable +import org.move.stdext.exists +import java.nio.file.Files +import javax.swing.JComponent +import kotlin.io.path.name + +class DownloadAptosDialog(val project: Project?): DialogWrapper(project, true) { + + private val cliListModel = CollectionComboBoxModel() + private val clisComboBox = ComboBox(cliListModel) + + val versionTextField = JBTextField() + val errorLabel = TextOrErrorLabel(UIUtil.getErrorIcon()) + + init { + title = "Download Aptos" + + val installDir = AptosInstaller.installDir + val regex = Regex("""aptos-cli-(\d+.\d+.\d+)""") + if (installDir.exists()) { + for (aptosFile in Files.walk(installDir, 0)) { + if (aptosFile.isValidExecutable()) { + val match = regex.find(aptosFile.name) + if (match != null) { + val (version) = match.destructured + cliListModel.add(AptosItem(version)) + } + } + } + } + + if (cliListModel.selectedItem == null && cliListModel.size != 0) { + cliListModel.selectedItem = cliListModel.items.first() + } + + init() + } + + override fun createCenterPanel(): JComponent { + return panel { + row { cell(clisComboBox) } + row("Version") { + cell(versionTextField).align(AlignX.FILL).columns(10) + cell(errorLabel) + } + row { + button("Download") { + // TODO: validate format + val version = versionTextField.text + ProgressManager.getInstance() + .run(object: Task.WithResult(project, title, true) { + override fun compute(indicator: ProgressIndicator) { + service() + .installAptosCli( + AptosItem(version), indicator, + onFinish = { aptosItem -> + val prevSize = cliListModel.size + cliListModel.add(aptosItem) + if (prevSize == 0) { + cliListModel.selectedItem = aptosItem + } + }) + } + }) + } + } + } + } +} + +class DownloadAptosAction: DumbAwareAction() { + + override fun actionPerformed(e: AnActionEvent) { + val dialog = DownloadAptosDialog(e.project) + dialog.showAndGet() + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/move/ide/actions/MoveEditSettingsAction.kt b/src/main/kotlin/org/move/ide/actions/MoveEditSettingsAction.kt new file mode 100644 index 000000000..b89e5bd68 --- /dev/null +++ b/src/main/kotlin/org/move/ide/actions/MoveEditSettingsAction.kt @@ -0,0 +1,22 @@ +package org.move.ide.actions + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.DumbAware +import org.move.cli.settings.PerProjectMoveConfigurable +import org.move.openapiext.showSettingsDialog + +class MoveEditSettingsAction : AnAction(), DumbAware { + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + override fun update(e: AnActionEvent) { + super.update(e) + e.presentation.isEnabledAndVisible = e.project != null + } + + override fun actionPerformed(e: AnActionEvent) { + e.project?.showSettingsDialog() + } +} diff --git a/src/main/kotlin/org/move/ide/annotator/HighlightingAnnotator.kt b/src/main/kotlin/org/move/ide/annotator/HighlightingAnnotator.kt index 4c4518c21..ad8914e5c 100644 --- a/src/main/kotlin/org/move/ide/annotator/HighlightingAnnotator.kt +++ b/src/main/kotlin/org/move/ide/annotator/HighlightingAnnotator.kt @@ -68,11 +68,13 @@ class HighlightingAnnotator: MvAnnotatorBase() { element.isInline -> MvColor.INLINE_FUNCTION element.isView -> MvColor.VIEW_FUNCTION element.isEntry -> MvColor.ENTRY_FUNCTION + element.selfParam != null -> MvColor.METHOD else -> MvColor.FUNCTION } if (element is MvStruct) return MvColor.STRUCT if (element is MvStructField) return MvColor.FIELD if (element is MvStructDotField) return MvColor.FIELD + if (element is MvMethodCall) return MvColor.METHOD_CALL if (element is MvStructPatField) return MvColor.FIELD if (element is MvStructLitField) return MvColor.FIELD if (element is MvConst) return MvColor.CONSTANT @@ -87,7 +89,14 @@ class HighlightingAnnotator: MvAnnotatorBase() { } private fun highlightBindingPat(bindingPat: MvBindingPat): MvColor { -// val msl = bindingPat.isMslLegacy() + val parent = bindingPat.parent + if (parent is MvFunctionParameter && bindingPat.name == "self") { + // check whether it's a first parameter + val parameterList = parent.parent as MvFunctionParameterList + if (parameterList.functionParameterList.indexOf(parent) == 0) { + return MvColor.SELF_PARAMETER + } + } val msl = bindingPat.isMslOnlyItem val itemTy = bindingPat.inference(msl)?.getPatType(bindingPat) return if (itemTy != null) { diff --git a/src/main/kotlin/org/move/ide/annotator/MvErrorAnnotator.kt b/src/main/kotlin/org/move/ide/annotator/MvErrorAnnotator.kt index 0cad4e182..391272834 100644 --- a/src/main/kotlin/org/move/ide/annotator/MvErrorAnnotator.kt +++ b/src/main/kotlin/org/move/ide/annotator/MvErrorAnnotator.kt @@ -1,11 +1,14 @@ package org.move.ide.annotator +import com.intellij.codeInsight.daemon.impl.HighlightRangeExtension import com.intellij.lang.annotation.AnnotationHolder import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile import org.move.ide.presentation.fullname import org.move.ide.presentation.itemDeclaredInModule import org.move.ide.utils.functionSignature import org.move.ide.utils.signature +import org.move.lang.MoveFile import org.move.lang.MvElementTypes.R_PAREN import org.move.lang.core.psi.* import org.move.lang.core.psi.ext.* @@ -20,10 +23,10 @@ import org.move.lang.moveProject import org.move.lang.utils.Diagnostic import org.move.lang.utils.addToHolder -class MvErrorAnnotator : MvAnnotatorBase() { +class MvErrorAnnotator: MvAnnotatorBase() { override fun annotateInternal(element: PsiElement, holder: AnnotationHolder) { val moveHolder = MvAnnotationHolder(holder) - val visitor = object : MvVisitor() { + val visitor = object: MvVisitor() { override fun visitConst(o: MvConst) = checkConstDef(moveHolder, o) override fun visitFunction(o: MvFunction) = checkFunction(moveHolder, o) @@ -34,95 +37,8 @@ class MvErrorAnnotator : MvAnnotatorBase() { override fun visitStructField(o: MvStructField) = checkDuplicates(moveHolder, o) - override fun visitPath(path: MvPath) { - val item = path.reference?.resolveWithAliases() - val msl = path.isMslScope - val realCount = path.typeArguments.size - val parent = path.parent - if (item == null && path.nullModuleRef && path.identifierName == "vector") { - val expectedCount = 1 - if (realCount != expectedCount) { - Diagnostic - .TypeArgumentsNumberMismatch(path, "vector", expectedCount, realCount) - .addToHolder(moveHolder) - } - return - } - val qualItem = item as? MvQualNamedElement ?: return - val qualName = qualItem.qualName ?: return - when { - qualItem is MvStruct && parent is MvPathType -> { - if (parent.ancestorStrict() != null) return - - if (realCount != 0) { - val typeArgumentList = - path.typeArgumentList ?: error("cannot be null if realCount != 0") - checkTypeArgumentList(typeArgumentList, qualItem, moveHolder) - } else { - val expectedCount = qualItem.typeParameters.size - if (expectedCount != 0) { - Diagnostic - .TypeArgumentsNumberMismatch( - path, - qualName.editorText(), - expectedCount, - realCount - ) - .addToHolder(moveHolder) - } - } - } - qualItem is MvStruct && parent is MvStructLitExpr -> { - // if any type param is passed, inference is disabled, so check fully - if (realCount != 0) { - val typeArgumentList = - path.typeArgumentList ?: error("cannot be null if realCount != 0") - checkTypeArgumentList(typeArgumentList, qualItem, moveHolder) - } - } - qualItem is MvFunction && parent is MvCallExpr -> { -// val expectedCount = qualItem.typeParameters.size - if (realCount != 0) { - // if any type param is passed, inference is disabled, so check fully - val typeArgumentList = - path.typeArgumentList ?: error("cannot be null if realCount != 0") - checkTypeArgumentList(typeArgumentList, qualItem, moveHolder) - } else { - val inference = parent.inference(msl) ?: return - if (parent.descendantHasTypeError(inference.typeErrors)) { - return - } - val callTy = inference.getCallExprType(parent) as? TyFunction ?: return - // if no type args are passed, check whether all type params are inferrable - if (callTy.needsTypeAnnotation()) { - Diagnostic - .NeedsTypeAnnotation(path) - .addToHolder(moveHolder) - } - } - } - qualItem is MvSchema && parent is MvSchemaLit -> { - val expectedCount = qualItem.typeParameters.size - if (realCount != 0) { - val typeArgumentList = - path.typeArgumentList ?: error("cannot be null if realCount != 0") - checkTypeArgumentList(typeArgumentList, qualItem, moveHolder) - } else { - // if no type args are passed, check whether all type params are inferrable - if (qualItem.requiredTypeParams.isNotEmpty() && expectedCount != 0) { - Diagnostic - .TypeArgumentsNumberMismatch( - path, - qualName.editorText(), - expectedCount, - realCount - ) - .addToHolder(moveHolder) - } - } - } - } - } + override fun visitPath(o: MvPath) = checkMethodOrPath(o, moveHolder) + override fun visitMethodCall(o: MvMethodCall) = checkMethodOrPath(o, moveHolder) override fun visitCallExpr(callExpr: MvCallExpr) { val msl = callExpr.path.isMslScope @@ -164,17 +80,26 @@ class MvErrorAnnotator : MvAnnotatorBase() { } override fun visitValueArgumentList(arguments: MvValueArgumentList) { - val parentExpr = arguments.parent + val parentCallable = arguments.parent val expectedCount = - when (parentExpr) { + when (parentCallable) { is MvCallExpr -> { - val msl = parentExpr.path.isMslScope + val msl = parentCallable.path.isMslScope val callTy = - parentExpr.inference(msl)?.getCallExprType(parentExpr) as? TyCallable ?: return + parentCallable.inference(msl)?.getCallableType(parentCallable) as? TyCallable + ?: return callTy.paramTypes.size } + is MvMethodCall -> { + val msl = parentCallable.isMslScope + val callTy = + parentCallable.inference(msl)?.getCallableType(parentCallable) as? TyCallable + ?: return + // 1 for self + callTy.paramTypes.size - 1 + } is MvAssertBangExpr -> { - if (parentExpr.identifier.text == "assert") { + if (parentCallable.identifier.text == "assert") { 2 } else { return @@ -270,6 +195,106 @@ class MvErrorAnnotator : MvAnnotatorBase() { checkDuplicates(holder, const, allConsts.asSequence()) } + private fun checkMethodOrPath(methodOrPath: MvMethodOrPath, holder: MvAnnotationHolder) { + val item = methodOrPath.reference?.resolveWithAliases() + val msl = methodOrPath.isMslScope + val realCount = methodOrPath.typeArguments.size + + val parent = methodOrPath.parent + if (item == null && methodOrPath is MvPath + && methodOrPath.nullModuleRef && methodOrPath.identifierName == "vector" + ) { + val expectedCount = 1 + if (realCount != expectedCount) { + Diagnostic + .TypeArgumentsNumberMismatch(methodOrPath, "vector", expectedCount, realCount) + .addToHolder(holder) + } + return + } + val qualItem = item as? MvQualNamedElement ?: return + val qualName = qualItem.qualName ?: return + when { + qualItem is MvStruct && parent is MvPathType -> { + if (parent.ancestorStrict() != null) return + + if (realCount != 0) { + val typeArgumentList = + methodOrPath.typeArgumentList ?: error("cannot be null if realCount != 0") + checkTypeArgumentList(typeArgumentList, qualItem, holder) + } else { + val expectedCount = qualItem.typeParameters.size + if (expectedCount != 0) { + Diagnostic + .TypeArgumentsNumberMismatch( + methodOrPath, + qualName.editorText(), + expectedCount, + realCount + ) + .addToHolder(holder) + } + } + } + qualItem is MvStruct && parent is MvStructLitExpr -> { + // if any type param is passed, inference is disabled, so check fully + if (realCount != 0) { + val typeArgumentList = + methodOrPath.typeArgumentList ?: error("cannot be null if realCount != 0") + checkTypeArgumentList(typeArgumentList, qualItem, holder) + } + } + qualItem is MvFunction -> { + val callable = + when (parent) { + is MvCallExpr -> parent + is MvDotExpr -> parent.methodCall + else -> null + } ?: return + if (realCount != 0) { + // if any type param is passed, inference is disabled, so check fully + val typeArgumentList = + methodOrPath.typeArgumentList ?: error("cannot be null if realCount != 0") + checkTypeArgumentList(typeArgumentList, qualItem, holder) + } else { + val inference = callable.inference(msl) ?: return + if (callable.descendantHasTypeError(inference.typeErrors)) { + return + } + val callTy = inference.getCallableType(callable) as? TyFunction ?: return + // if no type args are passed, check whether all type params are inferrable + if (callTy.needsTypeAnnotation()) { + val annotatedItem = + if (methodOrPath is MvMethodCall) methodOrPath.identifier else methodOrPath + Diagnostic + .NeedsTypeAnnotation(annotatedItem) + .addToHolder(holder) + } + } + } + qualItem is MvSchema && parent is MvSchemaLit -> { + val expectedCount = qualItem.typeParameters.size + if (realCount != 0) { + val typeArgumentList = + methodOrPath.typeArgumentList ?: error("cannot be null if realCount != 0") + checkTypeArgumentList(typeArgumentList, qualItem, holder) + } else { + // if no type args are passed, check whether all type params are inferrable + if (qualItem.requiredTypeParams.isNotEmpty() && expectedCount != 0) { + Diagnostic + .TypeArgumentsNumberMismatch( + methodOrPath, + qualName.editorText(), + expectedCount, + realCount + ) + .addToHolder(holder) + } + } + } + } + } + private fun checkTypeArgumentList( typeArgumentList: MvTypeArgumentList, item: MvTypeParametersOwner, diff --git a/src/main/kotlin/org/move/ide/colors/MvColor.kt b/src/main/kotlin/org/move/ide/colors/MvColor.kt index 0eed6ae28..307a535dd 100644 --- a/src/main/kotlin/org/move/ide/colors/MvColor.kt +++ b/src/main/kotlin/org/move/ide/colors/MvColor.kt @@ -40,9 +40,13 @@ enum class MvColor(humanName: String, default: TextAttributesKey? = null) { BUILTIN_FUNCTION_CALL("Functions//Builtins", Default.FUNCTION_CALL), + METHOD("Functions//Method", Default.INSTANCE_METHOD), + METHOD_CALL("Functions//Method call", Default.FUNCTION_CALL), + MACRO("Functions//Macro", Default.IDENTIFIER), KEYWORD("Keywords//Keyword", Default.KEYWORD), + SELF_PARAMETER("Keywords//Self Parameter", Default.KEYWORD), ABILITY("Types//Ability", Default.IDENTIFIER), PRIMITIVE_TYPE("Types//Primitive", Default.KEYWORD), diff --git a/src/main/kotlin/org/move/ide/folding/MvFoldingBuilder.kt b/src/main/kotlin/org/move/ide/folding/MvFoldingBuilder.kt index 663a06828..d77d9e2b3 100644 --- a/src/main/kotlin/org/move/ide/folding/MvFoldingBuilder.kt +++ b/src/main/kotlin/org/move/ide/folding/MvFoldingBuilder.kt @@ -14,7 +14,7 @@ import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.elementType import com.intellij.psi.util.nextLeaf -import org.move.cli.settings.collapseSpecs +import org.move.cli.settings.moveSettings import org.move.lang.MoveFile import org.move.lang.MoveParserDefinition.Companion.BLOCK_COMMENT import org.move.lang.MoveParserDefinition.Companion.EOL_DOC_COMMENT @@ -55,7 +55,7 @@ class MvFoldingBuilder : CustomFoldingBuilder(), DumbAware { } override fun isRegionCollapsedByDefault(node: ASTNode): Boolean { - return node.psi.project.collapseSpecs && node.elementType == MODULE_SPEC_BLOCK + return node.psi.project.moveSettings.foldSpecs && node.elementType == MODULE_SPEC_BLOCK || CodeFoldingSettings.getInstance().isDefaultCollapsedNode(node) } diff --git a/src/main/kotlin/org/move/ide/hints/InlayParameterHints.kt b/src/main/kotlin/org/move/ide/hints/InlayParameterHints.kt index f522cd1f5..ab11dbc33 100644 --- a/src/main/kotlin/org/move/ide/hints/InlayParameterHints.kt +++ b/src/main/kotlin/org/move/ide/hints/InlayParameterHints.kt @@ -6,7 +6,7 @@ import org.move.ide.utils.FunctionSignature import org.move.lang.core.psi.MvCallExpr import org.move.lang.core.psi.MvRefExpr import org.move.lang.core.psi.MvStructLitExpr -import org.move.lang.core.psi.ext.callArgumentExprs +import org.move.lang.core.psi.ext.argumentExprs import org.move.lang.core.psi.ext.startOffset @Suppress("UnstableApiUsage") @@ -17,7 +17,7 @@ object InlayParameterHints { val signature = FunctionSignature.resolve(elem) ?: return emptyList() return signature.parameters .map { it.name } - .zip(elem.callArgumentExprs) + .zip(elem.argumentExprs) .asSequence() .filter { (_, arg) -> arg != null } // don't show argument, if just function call / variable / struct literal diff --git a/src/main/kotlin/org/move/ide/inspections/MvAbilityCheckInspection.kt b/src/main/kotlin/org/move/ide/inspections/MvAbilityCheckInspection.kt index 0c484d096..c9ac1f8d0 100644 --- a/src/main/kotlin/org/move/ide/inspections/MvAbilityCheckInspection.kt +++ b/src/main/kotlin/org/move/ide/inspections/MvAbilityCheckInspection.kt @@ -47,7 +47,7 @@ class MvAbilityCheckInspection : MvLocalInspectionTool() { if (o.isMsl()) return val path = o.parent as? MvPath ?: return - val pathType = o.inference(false)?.getPathType(path) as? GenericTy ?: return + val pathType = o.inference(false)?.getMethodOrPathType(path) as? GenericTy ?: return val generics = pathType.item.generics for ((i, typeArgument) in o.typeArgumentList.withIndex()) { diff --git a/src/main/kotlin/org/move/ide/inspections/MvMissingAcquiresInspection.kt b/src/main/kotlin/org/move/ide/inspections/MvMissingAcquiresInspection.kt index 9f3245fd7..edc6a506b 100644 --- a/src/main/kotlin/org/move/ide/inspections/MvMissingAcquiresInspection.kt +++ b/src/main/kotlin/org/move/ide/inspections/MvMissingAcquiresInspection.kt @@ -5,6 +5,7 @@ import com.intellij.codeInspection.ProblemsHolder import org.move.ide.inspections.fixes.AddAcquiresFix import org.move.ide.presentation.fullnameNoArgs import org.move.lang.core.psi.* +import org.move.lang.core.psi.ext.MvCallable import org.move.lang.core.psi.ext.isInline import org.move.lang.core.types.infer.acquiresContext import org.move.lang.core.types.infer.inference @@ -18,18 +19,21 @@ class MvMissingAcquiresInspection : MvLocalInspectionTool() { override fun buildMvVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = object : MvVisitor() { - override fun visitCallExpr(callExpr: MvCallExpr) { - val outerFunction = callExpr.containingFunction ?: return + override fun visitCallExpr(o: MvCallExpr) = visitCallable(o) + override fun visitMethodCall(o: MvMethodCall) = visitCallable(o) + + private fun visitCallable(callable: MvCallable) { + val outerFunction = callable.containingFunction ?: return if (outerFunction.isInline) return - val acquiresContext = callExpr.moveProject?.acquiresContext ?: return + val acquiresContext = callable.moveProject?.acquiresContext ?: return val inference = outerFunction.inference(false) val existingTypes = acquiresContext.getFunctionTypes(outerFunction) val existingTypeNames = existingTypes.map { it.fullnameNoArgs() }.toSet() - val callExprTypes = acquiresContext.getCallTypes(callExpr, inference) + val callExprTypes = acquiresContext.getCallTypes(callable, inference) val currentModule = outerFunction.module ?: return val missingTypes = @@ -52,25 +56,11 @@ class MvMissingAcquiresInspection : MvLocalInspectionTool() { } } -// val itemTyVars = outerFunction.tyInfers -// val missingItems = inference.getAcquiredTypes(callExpr, outerSubst = itemTyVars) -//// .map { it.substituteOrUnknown(typeParameters) } -// .mapNotNull { ty -> -// when (ty) { -// is TyTypeParameter -> if (!declaredItems.any { it == ty.origin }) ty.origin else null -// is TyStruct -> { -// val notAcquired = ty.item.containingModule == currentModule -// && !declaredItems.any { it == ty.item } -// if (notAcquired) ty.item else null -// } -// else -> null -// } -// } if (missingTypes.isNotEmpty()) { val name = outerFunction.name ?: return val missingNames = missingTypes.mapNotNull { it.name } holder.registerProblem( - callExpr, + callable, "Function '$name' is not marked as 'acquires ${missingNames.joinToString()}'", ProblemHighlightType.GENERIC_ERROR, AddAcquiresFix(outerFunction, missingNames) diff --git a/src/main/kotlin/org/move/ide/inspections/MvUnusedAcquiresTypeInspection.kt b/src/main/kotlin/org/move/ide/inspections/MvUnusedAcquiresTypeInspection.kt index c61fc1ab5..9c5829025 100644 --- a/src/main/kotlin/org/move/ide/inspections/MvUnusedAcquiresTypeInspection.kt +++ b/src/main/kotlin/org/move/ide/inspections/MvUnusedAcquiresTypeInspection.kt @@ -24,7 +24,7 @@ class MvUnusedAcquiresTypeInspection : MvLocalInspectionTool() { val inference = function.inference(false) val callAcquiresTypes = mutableSetOf() - for (callExpr in inference.callExprTypes.keys) { + for (callExpr in inference.callableTypes.keys) { val types = acquiresContext.getCallTypes(callExpr, inference) callAcquiresTypes.addAll( types.map { it.fullnameNoArgs() }) diff --git a/src/main/kotlin/org/move/ide/inspections/ReplaceWithMethodCallInspection.kt b/src/main/kotlin/org/move/ide/inspections/ReplaceWithMethodCallInspection.kt new file mode 100644 index 000000000..8fb43bdd7 --- /dev/null +++ b/src/main/kotlin/org/move/ide/inspections/ReplaceWithMethodCallInspection.kt @@ -0,0 +1,50 @@ +package org.move.ide.inspections + +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import org.move.ide.inspections.fixes.ReplaceWithMethodCallFix +import org.move.lang.core.psi.* +import org.move.lang.core.psi.ext.getTyItemModule +import org.move.lang.core.psi.ext.isMsl +import org.move.lang.core.psi.ext.valueArguments +import org.move.lang.core.types.infer.foldTyTypeParameterWith +import org.move.lang.core.types.infer.inference +import org.move.lang.core.types.ty.* +import org.move.lang.core.types.ty.TyReference.Companion.isCompatibleWithAutoborrow +import org.move.lang.moveProject + +class ReplaceWithMethodCallInspection: MvLocalInspectionTool() { + override fun buildMvVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): MvVisitor { + return object: MvVisitor() { + override fun visitCallExpr(callExpr: MvCallExpr) { + val function = callExpr.path.reference?.resolve() as? MvFunction ?: return + val msl = callExpr.isMsl() + val inference = callExpr.inference(msl) ?: return + + val firstArgExpr = callExpr.valueArguments.firstOrNull()?.expr ?: return + val firstArgExprTy = inference.getExprType(firstArgExpr) + if (firstArgExprTy.hasTyUnknown) return + + val moveProject = callExpr.moveProject ?: return + val methodModule = function.module ?: return + val itemModule = moveProject.getTyItemModule(firstArgExprTy) ?: return + if (methodModule != itemModule) return + + val selfTy = function.selfParamTy(msl) + ?.foldTyTypeParameterWith { TyInfer.TyVar(it) } ?: return + if (selfTy.hasTyUnknown) return + + if (isCompatibleWithAutoborrow(firstArgExprTy, selfTy, msl)) { + // can be converted + holder.registerProblem( + callExpr, + "Can be replaced with method call", + ProblemHighlightType.WEAK_WARNING, + ReplaceWithMethodCallFix(callExpr) + ) + } + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/org/move/ide/inspections/fixes/ReplaceWithMethodCallFix.kt b/src/main/kotlin/org/move/ide/inspections/fixes/ReplaceWithMethodCallFix.kt new file mode 100644 index 000000000..303b32193 --- /dev/null +++ b/src/main/kotlin/org/move/ide/inspections/fixes/ReplaceWithMethodCallFix.kt @@ -0,0 +1,55 @@ +package org.move.ide.inspections.fixes + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import org.move.ide.inspections.DiagnosticFix +import org.move.lang.core.psi.* +import org.move.lang.core.psi.ext.valueArguments + +class ReplaceWithMethodCallFix(callExpr: MvCallExpr): DiagnosticFix(callExpr) { + override fun getText(): String = "Replace with method call" + + override fun invoke(project: Project, file: PsiFile, element: MvCallExpr) { + // can be converted + val psiFactory = element.project.psiFactory + + val fakeParams = element.valueArguments.drop(1).map { "1" }.toList() + val methodArgumentList = psiFactory.valueArgumentList(fakeParams) + val callArguments = element.valueArguments.drop(1) + for ((argument, callArgument) in methodArgumentList.valueArgumentList.zip(callArguments)) { + argument.replace(callArgument) + } + + var selfArgExpr = element.valueArguments.firstOrNull()?.expr ?: return + if (selfArgExpr is MvBorrowExpr) { + selfArgExpr = selfArgExpr.expr ?: return + } + when (selfArgExpr) { + // all AtomExpr list, same priority as MvDotExpr + is MvVectorLitExpr, is MvStructLitExpr, is MvTupleLitExpr, is MvParensExpr, is MvAnnotatedExpr, + is MvDotExpr, is MvIndexExpr, is MvCallExpr, is MvAssertBangExpr, is MvRefExpr, is MvLambdaExpr, + is MvLitExpr, is MvCodeBlockExpr -> { + // do nothing, those operations priorities are correct without parens + } + else -> { + val parensExpr = psiFactory.expr("(a)") + parensExpr.expr?.replace(selfArgExpr) + selfArgExpr = parensExpr + } + } + + val dotExpr = psiFactory.expr("1.${element.path.referenceName}()") + dotExpr.expr.replace(selfArgExpr) + + val typeArgumentList = element.path.typeArgumentList + if (typeArgumentList != null) { + dotExpr.methodCall?.typeArgumentList?.replace(typeArgumentList) + } else { + dotExpr.methodCall?.typeArgumentList?.delete() + } + + dotExpr.methodCall?.valueArgumentList?.replace(methodArgumentList) + + element.replace(dotExpr) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt b/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt index e0cfe853b..641be1858 100644 --- a/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt +++ b/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt @@ -12,14 +12,23 @@ import com.intellij.platform.DirectoryProjectGenerator import com.intellij.platform.DirectoryProjectGeneratorBase import com.intellij.platform.ProjectGeneratorPeer import org.move.cli.PluginApplicationDisposable -import org.move.cli.runConfigurations.InitProjectCli +import org.move.cli.runConfigurations.BlockchainCli import org.move.cli.settings.Blockchain +import org.move.cli.settings.Blockchain.APTOS +import org.move.cli.settings.Blockchain.SUI +import org.move.cli.settings.aptos.AptosExecType import org.move.cli.settings.moveSettings import org.move.ide.MoveIcons import org.move.openapiext.computeWithCancelableProgress +import org.move.stdext.toPathOrNull import org.move.stdext.unwrapOrThrow -data class MoveProjectConfig(val blockchain: Blockchain, val initCli: InitProjectCli) +data class MoveProjectConfig( + val blockchain: Blockchain, + val aptosExecType: AptosExecType, + val localAptosPath: String?, + val localSuiPath: String? +) class MoveProjectGenerator: DirectoryProjectGeneratorBase(), CustomStepProjectGenerator { @@ -38,7 +47,19 @@ class MoveProjectGenerator: DirectoryProjectGeneratorBase(), ) { val packageName = project.name val blockchain = projectConfig.blockchain - val projectCli = projectConfig.initCli + val projectCli = + when (blockchain) { + APTOS -> { + val aptosPath = + AptosExecType.aptosExecPath(projectConfig.aptosExecType, projectConfig.localAptosPath) + ?: error("validated before") + BlockchainCli.Aptos(aptosPath) + } + SUI -> { + val suiPath = projectConfig.localSuiPath?.toPathOrNull() ?: error("validated before") + BlockchainCli.Sui(suiPath) + } + } val manifestFile = project.computeWithCancelableProgress("Generating $blockchain project...") { val manifestFile = @@ -55,15 +76,19 @@ class MoveProjectGenerator: DirectoryProjectGeneratorBase(), project.moveSettings.modify { it.blockchain = blockchain when (projectCli) { - is InitProjectCli.Aptos -> { - it.aptosPath = projectCli.aptosExec.pathToSettingsFormat() + is BlockchainCli.Aptos -> { + it.aptosExecType = projectConfig.aptosExecType + it.localAptosPath = projectConfig.localAptosPath } - is InitProjectCli.Sui -> { - it.suiPath = projectCli.cliLocation.toString() + is BlockchainCli.Sui -> { + it.localSuiPath = projectConfig.localSuiPath } } } ProjectInitializationSteps.createDefaultCompileConfigurationIfNotExists(project) + // NOTE: + // this cannot be moved to a ProjectActivity, as Move.toml files + // are not created by the time those activities are executed ProjectInitializationSteps.openMoveTomlInEditor(project, manifestFile) } diff --git a/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt b/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt index 65b58fcf7..b650c6cea 100644 --- a/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt +++ b/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt @@ -6,15 +6,19 @@ package org.move.ide.newProject import com.intellij.openapi.Disposable -import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.util.Disposer import com.intellij.platform.GeneratorPeerImpl import com.intellij.ui.components.JBRadioButton -import com.intellij.ui.dsl.builder.* -import org.move.cli.runConfigurations.InitProjectCli +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.actionListener +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.selected import org.move.cli.settings.Blockchain +import org.move.cli.settings.MvProjectSettingsService +import org.move.cli.settings.aptos.AptosExecType import org.move.cli.settings.aptos.ChooseAptosCliPanel import org.move.cli.settings.isValidExecutable import org.move.cli.settings.sui.ChooseSuiCliPanel @@ -26,27 +30,33 @@ class MoveProjectGeneratorPeer(val parentDisposable: Disposable): GeneratorPeerI private val chooseAptosCliPanel = ChooseAptosCliPanel { checkValid?.run() } private val chooseSuiCliPanel = ChooseSuiCliPanel { checkValid?.run() } + private var blockchain: Blockchain + init { Disposer.register(parentDisposable, chooseAptosCliPanel) Disposer.register(parentDisposable, chooseSuiCliPanel) + + // set values from the default project settings + val defaultProjectSettings = + ProjectManager.getInstance().defaultProject.getService(MvProjectSettingsService::class.java) + blockchain = defaultProjectSettings.blockchain + + val localAptosPath = defaultProjectSettings.localAptosPath ?: Blockchain.aptosFromPATH() + val localSuiPath = defaultProjectSettings.localSuiPath ?: Blockchain.suiFromPATH() + chooseAptosCliPanel.data = + ChooseAptosCliPanel.Data(defaultProjectSettings.aptosExecType, localAptosPath) + chooseSuiCliPanel.data = ChooseSuiCliPanel.Data(localSuiPath) } private var checkValid: Runnable? = null - private var blockchain: Blockchain = Blockchain.SUI override fun getSettings(): MoveProjectConfig { - val initCli = - when (blockchain) { - Blockchain.APTOS -> { - InitProjectCli.Aptos(this.chooseAptosCliPanel.selectedAptosExec) - } - Blockchain.SUI -> { - val suiPath = this.chooseSuiCliPanel.getSuiCliPath().toPathOrNull() - ?: error("Should be validated separately") - InitProjectCli.Sui(suiPath) - } - } - return MoveProjectConfig(blockchain, initCli) + return MoveProjectConfig( + blockchain = blockchain, + aptosExecType = this.chooseAptosCliPanel.data.aptosExecType, + localAptosPath = this.chooseAptosCliPanel.data.localAptosPath, + localSuiPath = this.chooseSuiCliPanel.data.localSuiPath + ) } override fun getComponent(myLocationField: TextFieldWithBrowseButton, checkValid: Runnable): JComponent { @@ -55,29 +65,46 @@ class MoveProjectGeneratorPeer(val parentDisposable: Disposable): GeneratorPeerI } override fun getComponent(): JComponent { + val generatorPeer = this return panel { var aptosRadioButton: Cell? = null var suiRadioButton: Cell? = null - buttonsGroup("Blockchain") { row { - aptosRadioButton = radioButton("Aptos", Blockchain.APTOS) + aptosRadioButton = radioButton("Aptos") + .selected(generatorPeer.blockchain == Blockchain.APTOS) .actionListener { _, _ -> - blockchain = Blockchain.APTOS + generatorPeer.blockchain = Blockchain.APTOS checkValid?.run() } - suiRadioButton = radioButton("Sui", Blockchain.SUI) +// .bindSelected( +// { generatorPeer.blockchain == Blockchain.APTOS }, +// { +// generatorPeer.blockchain = Blockchain.APTOS +// checkValid?.run() +// } +// ) + suiRadioButton = radioButton("Sui") + .selected(generatorPeer.blockchain == Blockchain.SUI) .actionListener { _, _ -> - blockchain = Blockchain.SUI + generatorPeer.blockchain = Blockchain.SUI checkValid?.run() } +// .bindSelected( +// { generatorPeer.blockchain == Blockchain.SUI }, +// { +// generatorPeer.blockchain = Blockchain.SUI +// checkValid?.run() +// } +// ) } } - .bind({ blockchain }, { blockchain = it }) - chooseAptosCliPanel.attachToLayout(this) + chooseAptosCliPanel + .attachToLayout(this) .visibleIf(aptosRadioButton!!.selected) - chooseSuiCliPanel.attachToLayout(this) + chooseSuiCliPanel + .attachToLayout(this) .visibleIf(suiRadioButton!!.selected) } } @@ -85,14 +112,18 @@ class MoveProjectGeneratorPeer(val parentDisposable: Disposable): GeneratorPeerI override fun validate(): ValidationInfo? { when (blockchain) { Blockchain.APTOS -> { - val aptosPath = this.chooseAptosCliPanel.selectedAptosExec.toPathOrNull() - if (aptosPath == null || !aptosPath.isValidExecutable()) { + val panelData = this.chooseAptosCliPanel.data + val aptosExecPath = + AptosExecType.aptosExecPath(panelData.aptosExecType, panelData.localAptosPath) + if (aptosExecPath == null) { return ValidationInfo("Invalid path to $blockchain executable") } } Blockchain.SUI -> { - val suiPath = this.chooseSuiCliPanel.getSuiCliPath().toPathOrNull() - if (suiPath == null || !suiPath.isValidExecutable()) { + val suiExecPath = this.chooseSuiCliPanel.data.localSuiPath?.toPathOrNull() + if (suiExecPath == null + || !suiExecPath.isValidExecutable() + ) { return ValidationInfo("Invalid path to $blockchain executable") } } diff --git a/src/main/kotlin/org/move/ide/newProject/AlwaysRefreshProjectsAfterOpen.kt b/src/main/kotlin/org/move/ide/newProject/projectActivity/AlwaysRefreshProjectsAfterOpen.kt similarity index 94% rename from src/main/kotlin/org/move/ide/newProject/AlwaysRefreshProjectsAfterOpen.kt rename to src/main/kotlin/org/move/ide/newProject/projectActivity/AlwaysRefreshProjectsAfterOpen.kt index c6a4217f9..0608ba03f 100644 --- a/src/main/kotlin/org/move/ide/newProject/AlwaysRefreshProjectsAfterOpen.kt +++ b/src/main/kotlin/org/move/ide/newProject/projectActivity/AlwaysRefreshProjectsAfterOpen.kt @@ -1,4 +1,4 @@ -package org.move.ide.newProject +package org.move.ide.newProject.projectActivity import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project diff --git a/src/main/kotlin/org/move/ide/newProject/projectActivity/InferBlockchainTypeOnStartupActivity.kt b/src/main/kotlin/org/move/ide/newProject/projectActivity/InferBlockchainTypeOnStartupActivity.kt new file mode 100644 index 000000000..c72b555e6 --- /dev/null +++ b/src/main/kotlin/org/move/ide/newProject/projectActivity/InferBlockchainTypeOnStartupActivity.kt @@ -0,0 +1,34 @@ +package org.move.ide.newProject.projectActivity + +import com.intellij.ide.util.RunOnceUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.vfs.readText +import org.move.cli.settings.Blockchain.APTOS +import org.move.cli.settings.Blockchain.SUI +import org.move.cli.settings.moveSettings +import org.move.openapiext.rootDir + +class InferBlockchainTypeOnStartupActivity: ProjectActivity { + override suspend fun execute(project: Project) { + RunOnceUtil.runOnceForProject(project, ID) { + val moveToml = project.rootDir?.findChild("Move.toml") ?: return@runOnceForProject + val moveTomlText = moveToml.readText() + val blockchain = + when { + moveTomlText.contains("https://github.com/MystenLabs/sui.git") -> SUI + else -> APTOS + } + + if (project.moveSettings.blockchain != blockchain) { + project.moveSettings.modify { + it.blockchain = blockchain + } + } + } + } + + companion object { + const val ID = "org.move.InferBlockchainTypeOnStartupActivity" + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt b/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt index 1b19bc874..c9a83bc77 100644 --- a/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt +++ b/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt @@ -9,7 +9,7 @@ import org.move.cli.settings.* import org.move.lang.isMoveFile import org.move.lang.isMoveTomlManifestFile import org.move.openapiext.common.isUnitTestMode -import org.move.openapiext.showSettings +import org.move.openapiext.showSettingsDialog class InvalidBlockchainCliConfiguration(project: Project): MvEditorNotificationProvider(project), DumbAware { @@ -23,20 +23,20 @@ class InvalidBlockchainCliConfiguration(project: Project): MvEditorNotificationP if (!project.isTrusted()) return null if (isNotificationDisabled(file)) return null - val blockchain = project.blockchain + val blockchain = project.moveSettings.blockchain when (blockchain) { Blockchain.APTOS -> { - if (project.aptosExec.isValid()) return null + if (project.aptosExecPath.isValidExecutable()) return null } Blockchain.SUI -> { - if (project.suiPath.isValidExecutable()) return null + if (project.suiExecPath.isValidExecutable()) return null } } return EditorNotificationPanel().apply { text = "$blockchain CLI path is not provided or invalid" createActionLabel("Configure") { - project.showSettings() + project.showSettingsDialog() } createActionLabel("Do not show again") { disableNotification(file) diff --git a/src/main/kotlin/org/move/ide/notifications/MvEditorNotificationProvider.kt b/src/main/kotlin/org/move/ide/notifications/MvEditorNotificationProvider.kt index f2626aff8..ef073647d 100644 --- a/src/main/kotlin/org/move/ide/notifications/MvEditorNotificationProvider.kt +++ b/src/main/kotlin/org/move/ide/notifications/MvEditorNotificationProvider.kt @@ -7,8 +7,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.EditorNotificationPanel import com.intellij.ui.EditorNotificationProvider import com.intellij.ui.EditorNotifications -import org.move.cli.settings.MoveSettingsChangedEvent -import org.move.cli.settings.MoveSettingsListener +import org.move.cli.settings.MvProjectSettingsServiceBase.* import java.util.function.Function import javax.swing.JComponent @@ -16,9 +15,9 @@ fun updateAllNotifications(project: Project) { EditorNotifications.getInstance(project).updateAllNotifications() } -class UpdateNotificationsOnSettingsChangeListener(val project: Project) : MoveSettingsListener { +class UpdateNotificationsOnSettingsChangeListener(val project: Project): MoveSettingsListener { - override fun moveSettingsChanged(e: MoveSettingsChangedEvent) { + override fun > settingsChanged(e: SettingsChangedEventBase) { updateAllNotifications(project) } } diff --git a/src/main/kotlin/org/move/ide/notifications/NoMoveProjectDetectedNotificationProvider.kt b/src/main/kotlin/org/move/ide/notifications/NoMoveProjectDetectedNotificationProvider.kt index 77aae3c2c..bcae6a956 100644 --- a/src/main/kotlin/org/move/ide/notifications/NoMoveProjectDetectedNotificationProvider.kt +++ b/src/main/kotlin/org/move/ide/notifications/NoMoveProjectDetectedNotificationProvider.kt @@ -7,7 +7,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.EditorNotificationPanel import org.move.cli.moveProjectsService -import org.move.cli.settings.blockchain +import org.move.cli.settings.moveSettings import org.move.lang.isMoveFile import org.move.lang.isMoveTomlManifestFile import org.move.openapiext.common.isDispatchThread @@ -25,7 +25,7 @@ class NoMoveProjectDetectedNotificationProvider(project: Project): MvEditorNotif @Suppress("UnstableApiUsage") if (!project.isTrusted()) return null - val blockchain = project.blockchain + val blockchain = project.moveSettings.blockchain val moveProjectsService = project.moveProjectsService // HACK: Reloads projects once on an opening of any Move file, if not yet reloaded. // It should be invoked somewhere else where it's more appropriate, diff --git a/src/main/kotlin/org/move/ide/refactoring/MvRenameProcessor.kt b/src/main/kotlin/org/move/ide/refactoring/MvRenameProcessor.kt index e2848ea29..2299b9a3d 100644 --- a/src/main/kotlin/org/move/ide/refactoring/MvRenameProcessor.kt +++ b/src/main/kotlin/org/move/ide/refactoring/MvRenameProcessor.kt @@ -57,7 +57,14 @@ class MvRenameProcessor : RenamePsiElementProcessor() { val owner = element.owner usages.forEach { when (owner) { - is MvLetStmt -> { + is MvSchemaFieldStmt -> { + // NEW_SCHEMA_FIELD_NAME: OLD_VARIABLE_NAME + val schemaLitField = it.element as? MvSchemaLitField ?: return@forEach + val newSchemaLitField = + psiFactory.schemaLitField(newName, schemaLitField.referenceName) + schemaLitField.replace(newSchemaLitField) + } + else -> { val field = it.element?.maybeLitFieldParent // OLD_FIELD_NAME: NEW_VARIABLE_NAME when { @@ -73,13 +80,6 @@ class MvRenameProcessor : RenamePsiElementProcessor() { } } } - is MvSchemaFieldStmt -> { - // NEW_SCHEMA_FIELD_NAME: OLD_VARIABLE_NAME - val schemaLitField = it.element as? MvSchemaLitField ?: return@forEach - val newSchemaLitField = - psiFactory.schemaLitField(newName, schemaLitField.referenceName) - schemaLitField.replace(newSchemaLitField) - } } } } diff --git a/src/main/kotlin/org/move/lang/core/completion/CommonCompletionContributor.kt b/src/main/kotlin/org/move/lang/core/completion/CommonCompletionContributor.kt index 5beaa0666..90626d122 100644 --- a/src/main/kotlin/org/move/lang/core/completion/CommonCompletionContributor.kt +++ b/src/main/kotlin/org/move/lang/core/completion/CommonCompletionContributor.kt @@ -35,6 +35,7 @@ class CommonCompletionContributor : CompletionContributor() { ) extend(CompletionType.BASIC, MacrosCompletionProvider) extend(CompletionType.BASIC, VectorLiteralCompletionProvider) + extend(CompletionType.BASIC, MethodOrFieldCompletionProvider) } fun extend(type: CompletionType?, provider: MvCompletionProvider) { diff --git a/src/main/kotlin/org/move/lang/core/completion/FuncSignature.kt b/src/main/kotlin/org/move/lang/core/completion/FuncSignature.kt new file mode 100644 index 000000000..cb1ce1b0f --- /dev/null +++ b/src/main/kotlin/org/move/lang/core/completion/FuncSignature.kt @@ -0,0 +1,60 @@ +package org.move.lang.core.completion + +import org.move.ide.presentation.text +import org.move.lang.core.psi.MvFunction +import org.move.lang.core.psi.ext.name +import org.move.lang.core.psi.parameters +import org.move.lang.core.types.infer.TypeFoldable +import org.move.lang.core.types.infer.TypeFolder +import org.move.lang.core.types.infer.TypeVisitor +import org.move.lang.core.types.ty.Ty +import org.move.lang.core.types.ty.TyReference +import org.move.lang.core.types.ty.TyUnit + +data class FuncSignature( + private val params: Map, + private val retType: Ty, +): TypeFoldable { + + override fun innerFoldWith(folder: TypeFolder): FuncSignature { + return FuncSignature( + params = params.mapValues { (_, it) -> folder.fold(it) }, + retType = folder.fold(retType) + ) + } + + override fun innerVisitWith(visitor: TypeVisitor): Boolean = + params.values.any { visitor(it) } || visitor(retType) + + fun paramsText(): String { + return params.entries + .withIndex() + .joinToString(", ", prefix = "(", postfix = ")") { (i, value) -> + val (paramName, paramTy) = value + if (i == 0 && paramName == "self") { + when (paramTy) { + is TyReference -> "&${if (paramTy.isMut) "mut " else ""}self" + else -> "self" + } + } else { + "$paramName: ${paramTy.text(false)}" + } + } + } + + fun retTypeText(): String = retType.text(false) + + fun retTypeSuffix(): String { + return if (retType is TyUnit) "" else ": ${retTypeText()}" + } + + companion object { + fun fromFunction(function: MvFunction, msl: Boolean): FuncSignature { + val declaredType = function.declaredType(msl) + val params = function.parameters.zip(declaredType.paramTypes) + .associate { (param, paramTy) -> Pair(param.name, paramTy) } + val retType = declaredType.retType + return FuncSignature(params, retType) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/move/lang/core/completion/LookupElements.kt b/src/main/kotlin/org/move/lang/core/completion/LookupElements.kt index 8f13dc62a..3fd46d0d3 100644 --- a/src/main/kotlin/org/move/lang/core/completion/LookupElements.kt +++ b/src/main/kotlin/org/move/lang/core/completion/LookupElements.kt @@ -12,10 +12,8 @@ import org.move.ide.presentation.text import org.move.lang.core.psi.* import org.move.lang.core.psi.ext.* import org.move.lang.core.resolve.ContextScopeInfo -import org.move.lang.core.resolve.ref.Namespace import org.move.lang.core.types.infer.* import org.move.lang.core.types.ty.Ty -import org.move.lang.core.types.ty.TyFunction import org.move.lang.core.types.ty.TyUnknown const val KEYWORD_PRIORITY = 80.0 @@ -63,104 +61,95 @@ fun MvModule.createSelfLookup(): LookupElement { .withBoldness(true) } -fun MvNamedElement.createBaseLookupElement(ns: Set): LookupElementBuilder { +fun MvNamedElement.getLookupElementBuilder( + completionCtx: CompletionContext, + subst: Substitution = emptySubstitution, + structAsType: Boolean = false +): LookupElementBuilder { + val lookupElementBuilder = this.createLookupElementWithIcon() + val msl = completionCtx.isMsl() return when (this) { - is MvModuleUseSpeck -> { - val module = this.fqModuleRef?.reference?.resolve() - if (module != null) { - module.createBaseLookupElement(ns) + is MvFunction -> { + val signature = FuncSignature.fromFunction(this, msl).substitute(subst) + if (completionCtx.contextElement is MvMethodOrField) { + lookupElementBuilder + .withTailText(signature.paramsText()) + .withTypeText(signature.retTypeText()) } else { - this.createLookupElementWithIcon() + lookupElementBuilder + .withTailText(this.signatureText) + .withTypeText(this.outerFileName) } } - - is MvUseItem -> { - val namedItem = this.reference.resolve() - if (namedItem != null) { - namedItem.createBaseLookupElement(ns) - } else { - this.createLookupElementWithIcon() - } - } - - is MvFunction -> this.createLookupElementWithIcon() - .withTailText(this.signatureText) - .withTypeText(this.outerFileName) - - is MvSpecFunction -> this.createLookupElementWithIcon() - .withTailText(this.functionParameterList?.parametersText ?: "()") + is MvSpecFunction -> lookupElementBuilder + .withTailText(this.parameters.joinToSignature()) .withTypeText(this.returnType?.type?.text ?: "()") - is MvModule -> this.createLookupElementWithIcon() + is MvModule -> lookupElementBuilder .withTailText(this.addressRef()?.let { " ${it.text}" } ?: "") .withTypeText(this.containingFile?.name) is MvStruct -> { - val tailText = if (Namespace.TYPE !in ns) " { ... }" else "" - this.createLookupElementWithIcon() + val tailText = if (structAsType) "" else " { ... }" + lookupElementBuilder .withTailText(tailText) .withTypeText(this.containingFile?.name) } - is MvStructField -> this.createLookupElementWithIcon() - .withTypeText(this.typeAnnotation?.type?.text) - + is MvStructField -> { + val fieldTy = this.type?.loweredType(msl)?.substitute(subst) ?: TyUnknown + lookupElementBuilder + .withTypeText(fieldTy.text(false)) + } is MvConst -> { - val msl = this.isMslOnlyItem +// val msl = this.isMslOnlyItem val constTy = this.type?.loweredType(msl) ?: TyUnknown - this.createLookupElementWithIcon() + lookupElementBuilder .withTypeText(constTy.text(true)) } is MvBindingPat -> { - val msl = this.isMslOnlyItem - val inference = this.inference(msl) +// val msl = this.isMslOnlyItem + val bindingInference = this.inference(msl) // race condition sometimes happens, when file is too big, inference is not finished yet - val ty = inference?.getPatTypeOrUnknown(this) ?: TyUnknown - this.createLookupElementWithIcon() + val ty = bindingInference?.getPatTypeOrUnknown(this) ?: TyUnknown + lookupElementBuilder .withTypeText(ty.text(true)) } - is MvSchema -> this.createLookupElementWithIcon() + is MvSchema -> lookupElementBuilder .withTypeText(this.containingFile?.name) - else -> LookupElementBuilder.create(this) - .withLookupString(this.name ?: "") + else -> lookupElementBuilder } } data class CompletionContext( val contextElement: MvElement, - val namespaces: Set, val contextScopeInfo: ContextScopeInfo, val expectedTy: Ty? = null, -) +) { + fun isMsl(): Boolean = contextScopeInfo.isMslScope +} fun MvNamedElement.createLookupElement( completionContext: CompletionContext, + subst: Substitution = emptySubstitution, + structAsType: Boolean = false, priority: Double = DEFAULT_PRIORITY, insertHandler: InsertHandler = DefaultInsertHandler(completionContext), ): LookupElement { - val lookupElement = this.createBaseLookupElement(completionContext.namespaces) - val props = lookupProperties(this, completionContext) - return lookupElement - .withInsertHandler(insertHandler) - .withPriority(priority) - .toMvLookupElement(props) -} - -fun MvNamedElement.createCompletionLookupElement( - insertHandler: InsertHandler = DefaultInsertHandler(), - ns: Set = emptySet(), - priority: Double = DEFAULT_PRIORITY, - props: LookupElementProperties = LookupElementProperties() -): LookupElement { - val lookupElement = this.createBaseLookupElement(ns) - return lookupElement - .withInsertHandler(insertHandler) - .withPriority(priority) - .toMvLookupElement(props) + val builder = + this.getLookupElementBuilder( + completionContext, + subst = subst, + structAsType = structAsType + ) + .withInsertHandler(insertHandler) + .withPriority(priority) + val props = getLookupElementProperties(this, subst, completionContext) + return builder.toMvLookupElement(properties = props) } fun InsertionContext.addSuffix(suffix: String) { @@ -177,7 +166,7 @@ val InsertionContext.alreadyHasColonColon: Boolean val InsertionContext.alreadyHasSpace: Boolean get() = nextCharIs(' ') -private val InsertionContext.hasAngleBrackets: Boolean +val InsertionContext.hasAngleBrackets: Boolean get() = nextCharIs('<') fun InsertionContext.nextCharIs(c: Char): Boolean = @@ -199,7 +188,7 @@ private fun CharSequence.indexOfSkippingSpace(c: Char, startIndex: Int): Int? { fun LookupElementBuilder.withPriority(priority: Double): LookupElement = if (priority == DEFAULT_PRIORITY) this else PrioritizedLookupElement.withPriority(this, priority) -class AngleBracketsInsertHandler : InsertHandler { +class AngleBracketsInsertHandler: InsertHandler { override fun handleInsert(context: InsertionContext, item: LookupElement) { val document = context.document @@ -210,13 +199,40 @@ class AngleBracketsInsertHandler : InsertHandler { } } -open class DefaultInsertHandler(val completionContext: CompletionContext? = null) : InsertHandler { - override fun handleInsert(context: InsertionContext, item: LookupElement) { - val document = context.document +open class DefaultInsertHandler(val completionCtx: CompletionContext? = null): InsertHandler { + + final override fun handleInsert(context: InsertionContext, item: LookupElement) { val element = item.psiElement as? MvElement ?: return + handleInsert(element, context, item) + } + protected open fun handleInsert( + element: MvElement, + context: InsertionContext, + item: LookupElement + ) { + val document = context.document when (element) { - is MvFunctionLike -> handleFunctionInsert(context, element) + is MvFunctionLike -> { + val requiresExplicitTypeArguments = + element.requiresExplicitlyProvidedTypeArguments(completionCtx) + var suffix = "" + if (!context.hasAngleBrackets && requiresExplicitTypeArguments) { + suffix += "<>" + } + if (!context.hasAngleBrackets && !context.hasCallParens) { + suffix += "()" + } + val isMethodCall = context.getElementOfType() != null + val fnParameters = if (isMethodCall) element.parameters.drop(1) else element.parameters + val caretShift = when { + requiresExplicitTypeArguments -> 1 + fnParameters.isNotEmpty() -> 1 + else -> 2 + } + context.document.insertString(context.selectionEndOffset, suffix) + EditorModificationUtil.moveCaretRelatively(context.editor, caretShift) + } is MvSchema -> { if (element.hasTypeParameters) { if (!context.hasAngleBrackets) { @@ -239,40 +255,6 @@ open class DefaultInsertHandler(val completionContext: CompletionContext? = null } } } - - private fun handleFunctionInsert(context: InsertionContext, element: MvFunctionLike) { - val requiresExplicitTypes = run { - val msl = element.isMslOnlyItem - val callTy = element.declaredType(msl).substitute(element.tyInfers) as TyFunction - - val inferenceCtx = InferenceContext(msl) - callTy.paramTypes.forEach { - val resolvedParamType = it.foldTyInferWith { TyUnknown } - inferenceCtx.combineTypes(it, resolvedParamType) - } - val expectedTy = completionContext?.expectedTy - if (expectedTy != null && expectedTy !is TyUnknown) { - inferenceCtx.combineTypes(callTy.retType, expectedTy) - } - (inferenceCtx.resolveTypeVarsIfPossible(callTy) as TyFunction).needsTypeAnnotation() - } - - var suffix = "" - if (!context.hasAngleBrackets && requiresExplicitTypes) { - suffix += "<>" - } - if (!context.hasAngleBrackets && !context.hasCallParens) { - suffix += "()" - } - - val offset = when { - element.parameters.isNotEmpty() || requiresExplicitTypes -> 1 - else -> 2 - } - - context.document.insertString(context.selectionEndOffset, suffix) - EditorModificationUtil.moveCaretRelatively(context.editor, offset) - } } // When a user types `(` while completion, @@ -285,5 +267,5 @@ private fun InsertionContext.doNotAddOpenParenCompletionChar() { } } -inline fun InsertionContext.getElementOfType(strict: Boolean = false): T? = +inline fun InsertionContext.getElementOfType(strict: Boolean = false): T? = PsiTreeUtil.findElementOfClassAtOffset(file, tailOffset - 1, T::class.java, strict) diff --git a/src/main/kotlin/org/move/lang/core/completion/MvLookupElement.kt b/src/main/kotlin/org/move/lang/core/completion/MvLookupElement.kt index 7a1c94d54..2567f8bd9 100644 --- a/src/main/kotlin/org/move/lang/core/completion/MvLookupElement.kt +++ b/src/main/kotlin/org/move/lang/core/completion/MvLookupElement.kt @@ -3,10 +3,12 @@ package org.move.lang.core.completion import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementDecorator import org.move.lang.core.psi.* -import org.move.lang.core.types.infer.compatAbilities -import org.move.lang.core.types.infer.inference -import org.move.lang.core.types.infer.isCompatible -import org.move.lang.core.types.infer.loweredType +import org.move.lang.core.psi.ext.inferReceiverTy +import org.move.lang.core.psi.ext.structItem +import org.move.lang.core.types.infer.* +import org.move.lang.core.types.ty.TyFunction +import org.move.lang.core.types.ty.TyReference.Companion.autoborrow +import org.move.lang.core.types.ty.TyStruct import org.move.lang.core.types.ty.TyUnknown fun LookupElement.toMvLookupElement(properties: LookupElementProperties): MvLookupElement = @@ -15,7 +17,8 @@ fun LookupElement.toMvLookupElement(properties: LookupElementProperties): MvLook class MvLookupElement( delegate: LookupElement, val props: LookupElementProperties -) : LookupElementDecorator(delegate) { +): + LookupElementDecorator(delegate) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -54,24 +57,32 @@ data class LookupElementProperties( val typeHasAllRequiredAbilities: Boolean = false, ) -fun lookupProperties(element: MvNamedElement, context: CompletionContext): LookupElementProperties { +fun getLookupElementProperties( + element: MvNamedElement, + subst: Substitution, + context: CompletionContext +): LookupElementProperties { var props = LookupElementProperties() - val msl = context.contextScopeInfo.isMslScope val expectedTy = context.expectedTy if (expectedTy != null) { - val itemTy = when (element) { - is MvFunctionLike -> element.declaredType(msl).retType - is MvStruct -> element.declaredType(msl) - is MvConst -> element.type?.loweredType(msl) ?: TyUnknown - is MvBindingPat -> { - val inference = element.inference(msl) - // sometimes type inference won't be able to catch up with the completion, and this line crashes, - // so changing to infallible getPatTypeOrUnknown() - inference?.getPatTypeOrUnknown(element) ?: TyUnknown + val msl = context.isMsl() + val declaredTy = + when (element) { + is MvFunctionLike -> element.declaredType(msl).retType + is MvStruct -> element.declaredType(msl) + is MvConst -> element.type?.loweredType(msl) ?: TyUnknown + is MvBindingPat -> { + val inference = element.inference(msl) + // sometimes type inference won't be able to catch up with the completion, and this line crashes, + // so changing to infallible getPatTypeOrUnknown() + inference?.getPatTypeOrUnknown(element) ?: TyUnknown + } + is MvStructField -> element.type?.loweredType(msl) ?: TyUnknown + else -> TyUnknown } - else -> TyUnknown - } - // it is required for the TyInfer.TyVar to always have a different underlying unification table + val itemTy = declaredTy.substitute(subst) + + // NOTE: it is required for the TyInfer.TyVar to always have a different underlying unification table val isCompat = isCompatible(expectedTy, itemTy, msl) && compatAbilities(expectedTy, itemTy, msl) props = props.copy( isReturnTypeConformsToExpectedType = isCompat diff --git a/src/main/kotlin/org/move/lang/core/completion/providers/FQModuleCompletionProvider.kt b/src/main/kotlin/org/move/lang/core/completion/providers/FQModuleCompletionProvider.kt index b3101a6e3..e3144266f 100644 --- a/src/main/kotlin/org/move/lang/core/completion/providers/FQModuleCompletionProvider.kt +++ b/src/main/kotlin/org/move/lang/core/completion/providers/FQModuleCompletionProvider.kt @@ -13,13 +13,12 @@ import org.move.lang.core.psi.refItemScopes import org.move.lang.core.resolve.ContextScopeInfo import org.move.lang.core.resolve.letStmtScope import org.move.lang.core.resolve.processFQModuleRef -import org.move.lang.core.resolve.ref.Namespace import org.move.lang.core.types.Address import org.move.lang.core.types.address import org.move.lang.core.withParent import org.move.lang.moveProject -object FQModuleCompletionProvider : MvCompletionProvider() { +object FQModuleCompletionProvider: MvCompletionProvider() { override val elementPattern: ElementPattern get() = PlatformPatterns.psiElement() @@ -36,12 +35,11 @@ object FQModuleCompletionProvider : MvCompletionProvider() { ?: directParent.parent as MvFQModuleRef if (parameters.position !== fqModuleRef.referenceNameElement) return - val namespaces = setOf(Namespace.MODULE) val contextScopeInfo = ContextScopeInfo( letStmtScope = fqModuleRef.letStmtScope, refItemScopes = fqModuleRef.refItemScopes, ) - val completionContext = CompletionContext(fqModuleRef, namespaces, contextScopeInfo) + val completionContext = CompletionContext(fqModuleRef, contextScopeInfo) val moveProj = fqModuleRef.moveProject val positionAddress = fqModuleRef.addressRef.address(moveProj) diff --git a/src/main/kotlin/org/move/lang/core/completion/providers/ImportsCompletionProvider.kt b/src/main/kotlin/org/move/lang/core/completion/providers/ImportsCompletionProvider.kt index 1d2ed97ae..239230021 100644 --- a/src/main/kotlin/org/move/lang/core/completion/providers/ImportsCompletionProvider.kt +++ b/src/main/kotlin/org/move/lang/core/completion/providers/ImportsCompletionProvider.kt @@ -7,7 +7,8 @@ import com.intellij.patterns.ElementPattern import com.intellij.patterns.PlatformPatterns import com.intellij.psi.PsiElement import com.intellij.util.ProcessingContext -import org.move.lang.core.completion.createCompletionLookupElement +import org.move.lang.core.completion.CompletionContext +import org.move.lang.core.completion.createLookupElement import org.move.lang.core.completion.createSelfLookup import org.move.lang.core.psi.MvModule import org.move.lang.core.psi.MvUseItem @@ -34,9 +35,9 @@ object ImportsCompletionProvider: MvCompletionProvider() { result: CompletionResultSet ) { val itemImport = parameters.position.parent as MvUseItem - val moduleRef = itemImport.itemUseSpeck.fqModuleRef - if (parameters.position !== itemImport.referenceNameElement) return + + val moduleRef = itemImport.itemUseSpeck.fqModuleRef val referredModule = moduleRef.reference?.resolve() as? MvModule ?: return @@ -47,7 +48,7 @@ object ImportsCompletionProvider: MvCompletionProvider() { val vs = when { moduleRef.isSelf -> setOf(Visibility.Internal) - else -> Visibility.buildSetOfVisibilities(itemImport) + else -> Visibility.publicVisibilitiesFor(itemImport) } val ns = setOf(Namespace.NAME, Namespace.TYPE, Namespace.FUNCTION) val contextScopeInfo = @@ -55,9 +56,15 @@ object ImportsCompletionProvider: MvCompletionProvider() { letStmtScope = itemImport.letStmtScope, refItemScopes = itemImport.refItemScopes, ) + + val completionContext = CompletionContext(itemImport, contextScopeInfo) processModuleItems(referredModule, ns, vs, contextScopeInfo) { result.addElement( - it.element.createCompletionLookupElement(BasicInsertHandler(), ns = ns) + it.element.createLookupElement( + completionContext, + insertHandler = BasicInsertHandler(), + structAsType = true + ) ) false } diff --git a/src/main/kotlin/org/move/lang/core/completion/providers/MethodOrFieldCompletionProvider.kt b/src/main/kotlin/org/move/lang/core/completion/providers/MethodOrFieldCompletionProvider.kt new file mode 100644 index 000000000..d1c634aaf --- /dev/null +++ b/src/main/kotlin/org/move/lang/core/completion/providers/MethodOrFieldCompletionProvider.kt @@ -0,0 +1,86 @@ +package org.move.lang.core.completion.providers + +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.patterns.ElementPattern +import com.intellij.patterns.PlatformPatterns +import com.intellij.psi.PsiElement +import com.intellij.util.ProcessingContext +import org.jetbrains.annotations.VisibleForTesting +import org.move.lang.core.completion.* +import org.move.lang.core.psi.MvNamedElement +import org.move.lang.core.psi.ext.* +import org.move.lang.core.psi.refItemScopes +import org.move.lang.core.psi.tyInfers +import org.move.lang.core.resolve.ContextScopeInfo +import org.move.lang.core.resolve.letStmtScope +import org.move.lang.core.types.infer.InferenceContext +import org.move.lang.core.types.infer.Substitution +import org.move.lang.core.types.infer.substitute +import org.move.lang.core.types.ty.TyFunction +import org.move.lang.core.types.ty.TyReference +import org.move.lang.core.types.ty.TyStruct +import org.move.lang.core.types.ty.knownOrNull +import org.move.lang.core.withParent + +object MethodOrFieldCompletionProvider: MvCompletionProvider() { + override val elementPattern: ElementPattern + get() = + PlatformPatterns + .psiElement() + .withParent() + + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + val pos = parameters.position + val element = pos.parent as MvMethodOrField + + addMethodOrFieldVariants(element, result) + } + + @VisibleForTesting + fun addMethodOrFieldVariants(element: MvMethodOrField, result: CompletionResultSet) { + val msl = element.isMsl() + val receiverTy = element.inferReceiverTy(msl).knownOrNull() ?: return + val scopeInfo = ContextScopeInfo( + letStmtScope = element.letStmtScope, + refItemScopes = element.refItemScopes, + ) + val expectedTy = getExpectedTypeForEnclosingPathOrDotExpr(element, msl) + + val ctx = CompletionContext(element, scopeInfo, expectedTy) + + val structTy = receiverTy.derefIfNeeded() as? TyStruct + if (structTy != null) { + getFieldVariants(element, structTy, msl) + .forEach { (_, field) -> + val lookupElement = field.createLookupElement( + ctx, + subst = structTy.substitution + ) + result.addElement(lookupElement) + } + } + getMethodVariants(element, receiverTy, msl) + .forEach { (_, function) -> + val subst = function.tyInfers + val declaredFuncTy = function.declaredType(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) + inferenceCtx.combineTypes(declaredSelfTy, autoborrowedReceiverTy) + + val lookupElement = function.createLookupElement( + ctx, + subst = inferenceCtx.resolveTypeVarsIfPossible(subst) + ) + result.addElement(lookupElement) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/move/lang/core/completion/providers/ModulesCompletionProvider.kt b/src/main/kotlin/org/move/lang/core/completion/providers/ModulesCompletionProvider.kt index 459ba7890..6a430a27e 100644 --- a/src/main/kotlin/org/move/lang/core/completion/providers/ModulesCompletionProvider.kt +++ b/src/main/kotlin/org/move/lang/core/completion/providers/ModulesCompletionProvider.kt @@ -43,10 +43,10 @@ object ModulesCompletionProvider: MvCompletionProvider() { letStmtScope = refElement.letStmtScope, refItemScopes = refElement.refItemScopes, ) - val ctx = CompletionContext(refElement, namespaces, contextScopeInfo) + val completionCtx = CompletionContext(refElement, contextScopeInfo) processItems(refElement, namespaces, contextScopeInfo) { (name, element) -> result.addElement( - element.createLookupElement(ctx, priority = IMPORTED_MODULE_PRIORITY) + element.createLookupElement(completionCtx, priority = IMPORTED_MODULE_PRIORITY) ) processedNames.add(name) false @@ -72,10 +72,11 @@ object ModulesCompletionProvider: MvCompletionProvider() { }) candidates.forEach { candidate -> val lookupElement = - candidate.element.createCompletionLookupElement( - ImportInsertHandler(parameters, candidate), - importContext.namespaces, + candidate.element.createLookupElement( + completionCtx, + structAsType = Namespace.TYPE in importContext.namespaces, priority = UNIMPORTED_ITEM_PRIORITY, + insertHandler = ImportInsertHandler(parameters, candidate) ) result.addElement(lookupElement) } diff --git a/src/main/kotlin/org/move/lang/core/completion/providers/MvCompletionProvider.kt b/src/main/kotlin/org/move/lang/core/completion/providers/MvCompletionProvider.kt index aab652953..e012e77c4 100644 --- a/src/main/kotlin/org/move/lang/core/completion/providers/MvCompletionProvider.kt +++ b/src/main/kotlin/org/move/lang/core/completion/providers/MvCompletionProvider.kt @@ -48,12 +48,9 @@ class ImportInsertHandler( private val candidate: ImportCandidate ) : DefaultInsertHandler() { - override fun handleInsert(context: InsertionContext, item: LookupElement) { - super.handleInsert(context, item) -// context.commitDocument() -// val path = parameters.originalPosition?.parent as? MvPath ?: return + override fun handleInsert(element: MvElement, context: InsertionContext, item: LookupElement) { + super.handleInsert(element, context, item) context.import(candidate) -// candidate.import(path) } } diff --git a/src/main/kotlin/org/move/lang/core/completion/providers/MvPathCompletionProvider.kt b/src/main/kotlin/org/move/lang/core/completion/providers/MvPathCompletionProvider.kt index 2f7b47a6c..ae2eddbc9 100644 --- a/src/main/kotlin/org/move/lang/core/completion/providers/MvPathCompletionProvider.kt +++ b/src/main/kotlin/org/move/lang/core/completion/providers/MvPathCompletionProvider.kt @@ -12,7 +12,10 @@ import org.move.lang.core.completion.CompletionContext import org.move.lang.core.completion.UNIMPORTED_ITEM_PRIORITY import org.move.lang.core.completion.createLookupElement import org.move.lang.core.psi.* -import org.move.lang.core.psi.ext.* +import org.move.lang.core.psi.ext.ancestors +import org.move.lang.core.psi.ext.endOffset +import org.move.lang.core.psi.ext.isMslScope +import org.move.lang.core.psi.ext.isSelf import org.move.lang.core.resolve.* import org.move.lang.core.resolve.ref.MvReferenceElement import org.move.lang.core.resolve.ref.Namespace @@ -48,17 +51,23 @@ abstract class MvPathCompletionProvider: MvCompletionProvider() { val msl = pathElement.isMslScope val expectedTy = getExpectedTypeForEnclosingPathOrDotExpr(pathElement, msl) - val ctx = CompletionContext(pathElement, namespaces, pathScopeInfo, expectedTy) + val structAsType = this.namespace == Namespace.TYPE + val ctx = CompletionContext( + pathElement, + pathScopeInfo, + expectedTy + ) if (moduleRef != null) { val module = moduleRef.reference?.resolveWithAliases() as? MvModule ?: return val vs = when { moduleRef.isSelf -> setOf(Visibility.Internal) - else -> Visibility.buildSetOfVisibilities(pathElement) + else -> Visibility.publicVisibilitiesFor(pathElement) } processModuleItems(module, namespaces, vs, pathScopeInfo) { - val lookup = it.element.createLookupElement(ctx) + val lookup = + it.element.createLookupElement(ctx, structAsType = structAsType) result.addElement(lookup) false } @@ -72,7 +81,11 @@ abstract class MvPathCompletionProvider: MvCompletionProvider() { } processedNames.add(name) result.addElement( - element.createLookupElement(ctx, priority = element.completionPriority) + element.createLookupElement( + ctx, + structAsType = structAsType, + priority = element.completionPriority + ) ) false } @@ -97,6 +110,7 @@ abstract class MvPathCompletionProvider: MvCompletionProvider() { candidates.forEach { candidate -> val lookupElement = candidate.element.createLookupElement( ctx, + structAsType = structAsType, priority = UNIMPORTED_ITEM_PRIORITY, insertHandler = ImportInsertHandler(parameters, candidate) ) @@ -176,7 +190,7 @@ object SchemasCompletionProvider: MvPathCompletionProvider() { } } -private fun getExpectedTypeForEnclosingPathOrDotExpr(element: MvReferenceElement, msl: Boolean): Ty? { +fun getExpectedTypeForEnclosingPathOrDotExpr(element: MvReferenceElement, msl: Boolean): Ty? { for (ancestor in element.ancestors) { if (element.endOffset < ancestor.endOffset) continue if (element.endOffset > ancestor.endOffset) break diff --git a/src/main/kotlin/org/move/lang/core/completion/providers/SchemaFieldsCompletionProvider.kt b/src/main/kotlin/org/move/lang/core/completion/providers/SchemaFieldsCompletionProvider.kt index 53c11adc5..0a397bc6f 100644 --- a/src/main/kotlin/org/move/lang/core/completion/providers/SchemaFieldsCompletionProvider.kt +++ b/src/main/kotlin/org/move/lang/core/completion/providers/SchemaFieldsCompletionProvider.kt @@ -6,15 +6,17 @@ import com.intellij.patterns.ElementPattern import com.intellij.patterns.PlatformPatterns import com.intellij.psi.PsiElement import com.intellij.util.ProcessingContext -import org.move.lang.core.completion.createCompletionLookupElement +import org.move.lang.core.completion.CompletionContext +import org.move.lang.core.completion.createLookupElement import org.move.lang.core.psi.MvSchemaLitField import org.move.lang.core.psi.ext.fieldBindings import org.move.lang.core.psi.ext.fieldNames import org.move.lang.core.psi.ext.schema import org.move.lang.core.psi.ext.schemaLit +import org.move.lang.core.resolve.ContextScopeInfo import org.move.lang.core.withParent -object SchemaFieldsCompletionProvider : MvCompletionProvider() { +object SchemaFieldsCompletionProvider: MvCompletionProvider() { override val elementPattern: ElementPattern get() = PlatformPatterns .psiElement() @@ -31,8 +33,11 @@ object SchemaFieldsCompletionProvider : MvCompletionProvider() { val schema = schemaLit.schema ?: return val providedFieldNames = schemaLit.fieldNames + val completionCtx = CompletionContext(element, ContextScopeInfo.msl()) for (fieldBinding in schema.fieldBindings.filter { it.name !in providedFieldNames }) { - result.addElement(fieldBinding.createCompletionLookupElement()) + result.addElement( + fieldBinding.createLookupElement(completionCtx) + ) } } } diff --git a/src/main/kotlin/org/move/lang/core/completion/providers/StructFieldsCompletionProvider.kt b/src/main/kotlin/org/move/lang/core/completion/providers/StructFieldsCompletionProvider.kt index 9188016cc..6a5ca8c68 100644 --- a/src/main/kotlin/org/move/lang/core/completion/providers/StructFieldsCompletionProvider.kt +++ b/src/main/kotlin/org/move/lang/core/completion/providers/StructFieldsCompletionProvider.kt @@ -8,9 +8,11 @@ import com.intellij.patterns.StandardPatterns import com.intellij.psi.PsiElement import com.intellij.util.ProcessingContext import org.move.lang.core.MvPsiPatterns.bindingPat -import org.move.lang.core.completion.createCompletionLookupElement +import org.move.lang.core.completion.CompletionContext +import org.move.lang.core.completion.createLookupElement import org.move.lang.core.psi.* import org.move.lang.core.psi.ext.* +import org.move.lang.core.resolve.ContextScopeInfo import org.move.lang.core.withParent import org.move.lang.core.withSuperParent @@ -25,9 +27,6 @@ object StructFieldsCompletionProvider: MvCompletionProvider() { .withParent(), bindingPat() .withSuperParent(2), - PlatformPatterns - .psiElement() - .withParent(), ) override fun addCompletions( @@ -36,16 +35,19 @@ object StructFieldsCompletionProvider: MvCompletionProvider() { result: CompletionResultSet, ) { val pos = parameters.position - var element = pos.parent - if (element is MvBindingPat) element = element.parent + var element = pos.parent as? MvElement ?: return + + if (element is MvBindingPat) element = element.parent as MvElement + val completionCtx = CompletionContext(element, ContextScopeInfo.default()) when (element) { is MvStructPatField -> { val structPat = element.structPat addFieldsToCompletion( structPat.path.maybeStruct ?: return, structPat.patFieldNames, - result + result, + completionCtx ) } is MvStructLitField -> { @@ -53,28 +55,25 @@ object StructFieldsCompletionProvider: MvCompletionProvider() { addFieldsToCompletion( structLit.path.maybeStruct ?: return, structLit.fieldNames, - result + result, + completionCtx ) } - is MvStructDotField -> { - val receiverItem = element.receiverItem ?: return - receiverItem.fields - .forEach { - result.addElement( - it.createCompletionLookupElement() - ) - } - } } } + private fun addFieldsToCompletion( referredStruct: MvStruct, providedFieldNames: List, result: CompletionResultSet, + completionContext: CompletionContext, ) { for (field in referredStruct.fields.filter { it.name !in providedFieldNames }) { - result.addElement(field.createCompletionLookupElement()) + result.addElement( + field.createLookupElement(completionContext) + ) } } } + diff --git a/src/main/kotlin/org/move/lang/core/completion/providers/StructPatCompletionProvider.kt b/src/main/kotlin/org/move/lang/core/completion/providers/StructPatCompletionProvider.kt index f054c262b..de0aaf57a 100644 --- a/src/main/kotlin/org/move/lang/core/completion/providers/StructPatCompletionProvider.kt +++ b/src/main/kotlin/org/move/lang/core/completion/providers/StructPatCompletionProvider.kt @@ -6,7 +6,8 @@ import com.intellij.patterns.ElementPattern import com.intellij.patterns.PlatformPatterns import com.intellij.psi.PsiElement import com.intellij.util.ProcessingContext -import org.move.lang.core.completion.createCompletionLookupElement +import org.move.lang.core.completion.CompletionContext +import org.move.lang.core.completion.createLookupElement import org.move.lang.core.psi.MvBindingPat import org.move.lang.core.psi.MvLetStmt import org.move.lang.core.psi.containingModule @@ -40,8 +41,10 @@ object StructPatCompletionProvider: MvCompletionProvider() { letStmtScope = LetStmtScope.NONE, refItemScopes = bindingPat.namedItemScopes, ) + val completionCtx = CompletionContext(bindingPat, contextScopeInfo) processModuleItems(module, namespaces, setOf(Visibility.Internal), contextScopeInfo) { - val lookup = it.element.createCompletionLookupElement() + val lookup = + it.element.createLookupElement(completionCtx) result.addElement(lookup) false diff --git a/src/main/kotlin/org/move/lang/core/psi/MvElement.kt b/src/main/kotlin/org/move/lang/core/psi/MvElement.kt index 9cd044f38..7e7c4e4a8 100644 --- a/src/main/kotlin/org/move/lang/core/psi/MvElement.kt +++ b/src/main/kotlin/org/move/lang/core/psi/MvElement.kt @@ -32,6 +32,7 @@ val MvElement.namespaceModule: MvModule? } } + val MvElement.containingModule: MvModule? get() = ancestorStrict() val MvElement.containingModuleSpec: MvModuleSpec? get() = ancestorStrict() diff --git a/src/main/kotlin/org/move/lang/core/psi/MvFunctionLike.kt b/src/main/kotlin/org/move/lang/core/psi/MvFunctionLike.kt index 08efb2d38..58ee2c2e3 100644 --- a/src/main/kotlin/org/move/lang/core/psi/MvFunctionLike.kt +++ b/src/main/kotlin/org/move/lang/core/psi/MvFunctionLike.kt @@ -4,9 +4,14 @@ import com.intellij.ide.projectView.PresentationData import com.intellij.openapi.editor.colors.TextAttributesKey import org.move.ide.MoveIcons import org.move.lang.MvElementTypes +import org.move.lang.core.completion.CompletionContext import org.move.lang.core.psi.ext.* import org.move.lang.core.stubs.MvModuleStub +import org.move.lang.core.types.infer.InferenceContext +import org.move.lang.core.types.infer.foldTyInferWith import org.move.lang.core.types.infer.loweredType +import org.move.lang.core.types.infer.substitute +import org.move.lang.core.types.ty.Ty import org.move.lang.core.types.ty.TyFunction import org.move.lang.core.types.ty.TyLambda import org.move.lang.core.types.ty.TyUnknown @@ -108,8 +113,40 @@ val MvFunctionLike.script: MvScript? val MvFunctionLike.signatureText: String get() { - val params = this.functionParameterList?.parametersText ?: "()" - val returnTypeText = this.returnType?.type?.text ?: "" - val returnType = if (returnTypeText == "") "" else ": $returnTypeText" - return "$params$returnType" + val paramsText = this.parameters.joinToSignature() + val retType = this.returnType?.type?.text ?: "" + val retTypeSuffix = if (retType == "") "" else ": $retType" + return "$paramsText$retTypeSuffix" } + +val MvFunction.selfParam: MvFunctionParameter? get() = + this.parameters.firstOrNull()?.takeIf { it.name == "self" } + +fun MvFunction.selfParamTy(msl: Boolean): Ty? = this.selfParam?.type?.loweredType(msl) + +val MvFunction.isMethod get() = selfParam != null + +val MvFunction.selfSignatureText: String + get() { + val paramsText = this.parameters.drop(1).joinToSignature() + val retType = this.returnType?.type?.text ?: "" + val retTypeSuffix = if (retType == "") "" else ": $retType" + return "$paramsText$retTypeSuffix" + } + +fun MvFunctionLike.requiresExplicitlyProvidedTypeArguments(completionContext: CompletionContext?): Boolean + { + val msl = this.isMslOnlyItem + val callTy = this.declaredType(msl).substitute(this.tyInfers) as TyFunction + + val inferenceCtx = InferenceContext(msl) + callTy.paramTypes.forEach { + inferenceCtx.combineTypes(it, it.foldTyInferWith { TyUnknown }) + } + val expectedTy = completionContext?.expectedTy + if (expectedTy != null && expectedTy !is TyUnknown) { + inferenceCtx.combineTypes(callTy.retType, expectedTy) + } + val resolvedCallTy = inferenceCtx.resolveTypeVarsIfPossible(callTy) as TyFunction + return resolvedCallTy.needsTypeAnnotation() + } \ No newline at end of file diff --git a/src/main/kotlin/org/move/lang/core/psi/MvPsiFactory.kt b/src/main/kotlin/org/move/lang/core/psi/MvPsiFactory.kt index efb4fb7c8..2a7b7b3c2 100644 --- a/src/main/kotlin/org/move/lang/core/psi/MvPsiFactory.kt +++ b/src/main/kotlin/org/move/lang/core/psi/MvPsiFactory.kt @@ -165,6 +165,12 @@ class MvPsiFactory(val project: Project) { ?: error("Failed to create a type parameter from text: `$text`") } + fun valueArgumentList(parameters: List): MvValueArgumentList { + return createFromText( + "module 0x1::main { fun main() { call(${parameters.joinToString(", ")}); } }" + ) ?: error("unreachable") + } + fun path(text: String): MvPath { return createFromText("module 0x1::_DummyModule { fun main() { $text(); } } ") ?: error("`$text`") diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvCallExpr.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvCallExpr.kt index c3f77d227..12d74289b 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/MvCallExpr.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvCallExpr.kt @@ -2,16 +2,14 @@ package org.move.lang.core.psi.ext import org.move.lang.core.psi.* -val MvCallExpr.typeArguments: List get() = this.path.typeArguments +interface MvCallable: MvElement { + val valueArgumentList: MvValueArgumentList? +} -val MvCallExpr.valueArguments: List - get() = - this.valueArgumentList?.valueArgumentList.orEmpty() -val MvAssertBangExpr.valueArguments: List +val MvCallable.valueArguments: List get() = this.valueArgumentList?.valueArgumentList.orEmpty() -val MvCallExpr.callArgumentExprs: List get() = this.valueArguments.map { it.expr } +val MvCallable.argumentExprs: List get() = this.valueArguments.map { it.expr } -val MvAssertBangExpr.callArgumentExprs: List get() = this.valueArguments.map { it.expr } diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvElement.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvElement.kt index d7d2807c8..9a7241946 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/MvElement.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvElement.kt @@ -47,7 +47,7 @@ val MvNamedElement.isMslOnlyItem: Boolean return false } -val MvPath.isMslScope: Boolean get() = this.isMslInner() +val MvMethodOrPath.isMslScope get() = this.isMslInner() val MvModuleRef.isMslScope: Boolean get() = this.isMslInner() diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvFunctionParameterList.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvFunctionParameterList.kt index 8df35370f..22799a0ce 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/MvFunctionParameterList.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvFunctionParameterList.kt @@ -1,11 +1,10 @@ package org.move.lang.core.psi.ext +import org.move.lang.core.psi.MvFunctionParameter import org.move.lang.core.psi.MvFunctionParameterList import org.move.utils.SignatureUtils -val MvFunctionParameterList.parametersText: String - get() { - return SignatureUtils.joinParameters(this.functionParameterList.map { - Pair(it.bindingPat.name, it.typeAnnotation?.type?.text) - }) - } +fun List.joinToSignature(): String { + val parameterPairs = this.map { Pair(it.bindingPat.name, it.typeAnnotation?.type?.text) } + return SignatureUtils.joinParameters(parameterPairs) +} diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvMethodCall.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvMethodCall.kt new file mode 100644 index 000000000..44de1890c --- /dev/null +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvMethodCall.kt @@ -0,0 +1,85 @@ +package org.move.lang.core.psi.ext + +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import org.move.cli.MoveProject +import org.move.lang.core.completion.getOriginalOrSelf +import org.move.lang.core.psi.* +import org.move.lang.core.resolve.ScopeItem +import org.move.lang.core.resolve.ref.MvPolyVariantReference +import org.move.lang.core.resolve.ref.MvPolyVariantReferenceBase +import org.move.lang.core.resolve.ref.Visibility +import org.move.lang.core.types.address +import org.move.lang.core.types.infer.foldTyTypeParameterWith +import org.move.lang.core.types.infer.inference +import org.move.lang.core.types.ty.* +import org.move.lang.moveProject +import org.move.stdext.wrapWithList + +typealias MatchSequence = Sequence> + +fun MatchSequence.filterByName(refName: String): Sequence { + return this + .filter { it.name == refName } + .map { it.element } +} + +fun MoveProject.getTyItemModule(ty: Ty): MvModule? { + val norefTy = ty.derefIfNeeded() + return when (norefTy) { + is TyVector -> { + this + .getModulesFromIndex("vector") + .firstOrNull { + val moduleAddress = it.address(this)?.canonicalValue(this) + moduleAddress == "0x00000000000000000000000000000001" + } + } + is TyStruct -> norefTy.item.module + else -> null + } +} + +fun getMethodVariants(element: MvMethodOrField, receiverTy: Ty, msl: Boolean): MatchSequence { + val moveProject = element.moveProject ?: return emptySequence() + val receiverTyItemModule = moveProject.getTyItemModule(receiverTy) ?: return emptySequence() + + val visibilities = Visibility.publicVisibilitiesFor(element).toMutableSet() + if (element.containingModule == receiverTyItemModule) { + visibilities.add(Visibility.Internal) + } + val functions = + visibilities.flatMap { receiverTyItemModule.visibleFunctions(it) } + .filter { + val selfTy = it.selfParamTy(msl) ?: return@filter false + // need to use TyVar here, loweredType() erases them + val selfTyWithTyVars = + selfTy.foldTyTypeParameterWith { tp -> TyInfer.TyVar(tp) } + TyReference.isCompatibleWithAutoborrow(receiverTy, selfTyWithTyVars, msl) + } + return functions + .filter { it.name != null } + .map { ScopeItem(it.name!!, it) }.asSequence() +} + +class MvMethodCallReferenceImpl( + element: MvMethodCall +): + MvPolyVariantReferenceBase(element) { + + override fun multiResolve(): List { + val msl = element.isMsl() + val receiverExpr = element.receiverExpr + val inference = receiverExpr.inference(msl) ?: return emptyList() + return inference.getResolvedMethod(element).wrapWithList() + } + + override fun isReferenceTo(element: PsiElement): Boolean = + element is MvFunction && super.isReferenceTo(element) +} + +abstract class MvMethodCallMixin(node: ASTNode): MvElementImpl(node), + MvMethodCall { + + override fun getReference(): MvPolyVariantReference = MvMethodCallReferenceImpl(this) +} \ No newline at end of file diff --git a/src/main/kotlin/org/move/lang/core/types/infer/ResolveHelpers.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvMethodOrField.kt similarity index 62% rename from src/main/kotlin/org/move/lang/core/types/infer/ResolveHelpers.kt rename to src/main/kotlin/org/move/lang/core/psi/ext/MvMethodOrField.kt index 36970e403..32d7fe1f9 100644 --- a/src/main/kotlin/org/move/lang/core/types/infer/ResolveHelpers.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvMethodOrField.kt @@ -1,14 +1,21 @@ -package org.move.lang.core.types.infer +package org.move.lang.core.psi.ext import org.move.lang.core.psi.MvDotExpr -import org.move.lang.core.psi.ext.ancestorOrSelf +import org.move.lang.core.psi.MvExpr +import org.move.lang.core.resolve.ref.MvMandatoryReferenceElement +import org.move.lang.core.types.infer.MvInferenceContextOwner +import org.move.lang.core.types.infer.inferTypesIn +import org.move.lang.core.types.infer.inference import org.move.lang.core.types.ty.Ty -import org.move.lang.core.types.ty.TyReference -import org.move.lang.core.types.ty.TyStruct import org.move.lang.core.types.ty.TyUnknown -fun MvDotExpr.inferReceiverTy(msl: Boolean): Ty { - val receiverExpr = this.expr +interface MvMethodOrField: MvMandatoryReferenceElement + +val MvMethodOrField.dotExpr: MvDotExpr get() = parent as MvDotExpr +val MvMethodOrField.receiverExpr: MvExpr get() = dotExpr.expr + +fun MvMethodOrField.inferReceiverTy(msl: Boolean): Ty { + val receiverExpr = this.receiverExpr val inference = receiverExpr.inference(msl) ?: return TyUnknown val receiverTy = inference.getExprTypeOrNull(receiverExpr) ?: run { @@ -22,11 +29,6 @@ fun MvDotExpr.inferReceiverTy(msl: Boolean): Ty { val noCacheInference = inferTypesIn(inferenceOwner, msl) noCacheInference.getExprType(receiverExpr) } - - val innerTy = when (receiverTy) { - is TyReference -> receiverTy.innerTy() as? TyStruct ?: TyUnknown - is TyStruct -> receiverTy - else -> TyUnknown - } - return innerTy + return receiverTy } + diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvMethodOrPath.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvMethodOrPath.kt new file mode 100644 index 000000000..6b94eba29 --- /dev/null +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvMethodOrPath.kt @@ -0,0 +1,11 @@ +package org.move.lang.core.psi.ext + +import org.move.lang.core.psi.MvTypeArgument +import org.move.lang.core.psi.MvTypeArgumentList +import org.move.lang.core.resolve.ref.MvReferenceElement + +val MvMethodOrPath.typeArguments: List get() = typeArgumentList?.typeArgumentList.orEmpty() + +interface MvMethodOrPath: MvReferenceElement { + val typeArgumentList: MvTypeArgumentList? +} \ No newline at end of file diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvPath.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvPath.kt index e945e2d17..003f076ae 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/MvPath.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvPath.kt @@ -34,7 +34,7 @@ val MvPath.isUpdateFieldArg2: Boolean ?.let { if (it.path.textMatches("update_field")) it else null } ?.let { val expr = this.ancestorStrict() ?: return@let -1 - it.callArgumentExprs.indexOf(expr) + it.argumentExprs.indexOf(expr) } return ind == 1 } @@ -47,9 +47,6 @@ val MvPath.nullModuleRef: Boolean val MvPath.isQualPath: Boolean get() = !this.nullModuleRef -val MvPath.typeArguments: List - get() = typeArgumentList?.typeArgumentList.orEmpty() - val MvPath.maybeStruct get() = reference?.resolveWithAliases() as? MvStruct val MvPath.maybeSchema get() = reference?.resolveWithAliases() as? MvSchema diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvStructDotField.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvStructDotField.kt index 3f88e5678..13fa3e72a 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/MvStructDotField.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvStructDotField.kt @@ -1,39 +1,35 @@ package org.move.lang.core.psi.ext import com.intellij.lang.ASTNode -import org.move.lang.core.completion.getOriginalOrSelf import org.move.lang.core.psi.* +import org.move.lang.core.resolve.ScopeItem import org.move.lang.core.resolve.ref.MvPolyVariantReference -import org.move.lang.core.resolve.ref.MvPolyVariantReferenceCached -import org.move.lang.core.types.infer.inferReceiverTy +import org.move.lang.core.resolve.ref.MvPolyVariantReferenceBase +import org.move.lang.core.types.infer.inference +import org.move.lang.core.types.ty.Ty import org.move.lang.core.types.ty.TyStruct +import org.move.stdext.wrapWithList -val MvStructDotField.receiverItem: MvStruct? - get() { - val dotExpr = - (this.parent as? MvDotExpr)?.getOriginalOrSelf() ?: return null - val msl = dotExpr.isMsl() - val innerTy = dotExpr.inferReceiverTy(msl) - if (innerTy !is TyStruct) return null - val structItem = innerTy.item - if (!msl) { - // cannot resolve field if not in the same module as struct definition - val dotExprModule = dotExpr.namespaceModule ?: return null - if (structItem.containingModule != dotExprModule) return null - } - return structItem +fun getFieldVariants(element: MvMethodOrField, receiverTy: TyStruct, msl: Boolean): MatchSequence { + val structItem = receiverTy.item + if (!msl) { + // cannot resolve field if not in the same module as struct definition + val dotExprModule = element.namespaceModule ?: return emptySequence() + if (structItem.containingModule != dotExprModule) return emptySequence() } + return structItem.fields + .map { ScopeItem(it.name, it) }.asSequence() +} class MvStructDotFieldReferenceImpl( element: MvStructDotField -): MvPolyVariantReferenceCached(element) { - - override fun multiResolveInner(): List { - val receiverItem = element.receiverItem ?: return emptyList() +): MvPolyVariantReferenceBase(element) { - val referenceName = element.referenceName - return receiverItem.fields - .filter { it.name == referenceName } + override fun multiResolve(): List { + val msl = element.isMsl() + val receiverExpr = element.receiverExpr + val inference = receiverExpr.inference(msl) ?: return emptyList() + return inference.getResolvedField(element).wrapWithList() } } diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvUseItem.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvUseItem.kt index 8a874d847..2d6d79d47 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/MvUseItem.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvUseItem.kt @@ -63,7 +63,7 @@ class MvUseItemReferenceElement( Namespace.SCHEMA, Namespace.CONST ) - val vs = Visibility.buildSetOfVisibilities(fqModuleRef) + val vs = Visibility.publicVisibilitiesFor(fqModuleRef) // import has MAIN+VERIFY, and TEST if it or any of the parents has test val useItemScopes = mutableSetOf(NamedItemScope.MAIN, NamedItemScope.VERIFY) diff --git a/src/main/kotlin/org/move/lang/core/resolve/NameResolution.kt b/src/main/kotlin/org/move/lang/core/resolve/NameResolution.kt index 426f84569..73f02f4a3 100644 --- a/src/main/kotlin/org/move/lang/core/resolve/NameResolution.kt +++ b/src/main/kotlin/org/move/lang/core/resolve/NameResolution.kt @@ -3,7 +3,11 @@ package org.move.lang.core.resolve import com.intellij.psi.search.GlobalSearchScope import org.move.lang.MoveFile import org.move.lang.core.psi.* +import org.move.lang.core.psi.NamedItemScope.MAIN +import org.move.lang.core.psi.NamedItemScope.VERIFY import org.move.lang.core.psi.ext.* +import org.move.lang.core.resolve.LetStmtScope.EXPR_STMT +import org.move.lang.core.resolve.LetStmtScope.NONE import org.move.lang.core.resolve.ref.MvReferenceElement import org.move.lang.core.resolve.ref.Namespace import org.move.lang.core.resolve.ref.Visibility @@ -26,6 +30,12 @@ data class ContextScopeInfo( if (!itemElement.isVisibleInContext(this.refItemScopes)) return false return true } + + companion object { + /// really does not affect anything, created just to allow creating CompletionContext everywhere + fun default(): ContextScopeInfo = ContextScopeInfo(setOf(MAIN), NONE) + fun msl(): ContextScopeInfo = ContextScopeInfo(setOf(VERIFY), EXPR_STMT) + } } fun processItems( diff --git a/src/main/kotlin/org/move/lang/core/resolve/ref/MovePathReferenceImpl.kt b/src/main/kotlin/org/move/lang/core/resolve/ref/MovePathReferenceImpl.kt index 62c26468e..a4c4c768e 100644 --- a/src/main/kotlin/org/move/lang/core/resolve/ref/MovePathReferenceImpl.kt +++ b/src/main/kotlin/org/move/lang/core/resolve/ref/MovePathReferenceImpl.kt @@ -15,7 +15,7 @@ class MvPathReferenceImpl( override fun multiResolveInner(): List { val pathNamespaces = element.namespaces() - val vs = Visibility.buildSetOfVisibilities(element) + val vs = Visibility.publicVisibilitiesFor(element) val contextScopeInfo = ContextScopeInfo( refItemScopes = element.refItemScopes, diff --git a/src/main/kotlin/org/move/lang/core/resolve/ref/MoveReferenceBase.kt b/src/main/kotlin/org/move/lang/core/resolve/ref/MoveReferenceBase.kt index c2452de2c..1500fc1f8 100644 --- a/src/main/kotlin/org/move/lang/core/resolve/ref/MoveReferenceBase.kt +++ b/src/main/kotlin/org/move/lang/core/resolve/ref/MoveReferenceBase.kt @@ -2,7 +2,9 @@ package org.move.lang.core.resolve.ref import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementResolveResult import com.intellij.psi.PsiPolyVariantReferenceBase +import com.intellij.psi.ResolveResult import org.move.lang.core.psi.MvNamedElement import org.move.utils.doRenameIdentifier @@ -39,6 +41,9 @@ abstract class MvPolyVariantReferenceBase(element: T): final override fun resolve(): MvNamedElement? = super.resolve() as? MvNamedElement + override fun multiResolve(incompleteCode: Boolean): Array = + multiResolve().map { PsiElementResolveResult(it) }.toTypedArray() + override fun equals(other: Any?): Boolean = other is MvPolyVariantReferenceBase<*> && element === other.element diff --git a/src/main/kotlin/org/move/lang/core/resolve/ref/Namespace.kt b/src/main/kotlin/org/move/lang/core/resolve/ref/Namespace.kt index 788dd8c8f..16c381924 100644 --- a/src/main/kotlin/org/move/lang/core/resolve/ref/Namespace.kt +++ b/src/main/kotlin/org/move/lang/core/resolve/ref/Namespace.kt @@ -19,7 +19,7 @@ sealed class Visibility { fun local(): Set = setOf(Public, Internal) fun none(): Set = setOf() - fun buildSetOfVisibilities(element: MvElement): Set { + fun publicVisibilitiesFor(element: MvElement): Set { val vs = mutableSetOf(Public) val containingModule = element.containingModule if (containingModule != null) { diff --git a/src/main/kotlin/org/move/lang/core/types/infer/Acquires.kt b/src/main/kotlin/org/move/lang/core/types/infer/Acquires.kt index 67ab3f4ed..8365e5640 100644 --- a/src/main/kotlin/org/move/lang/core/types/infer/Acquires.kt +++ b/src/main/kotlin/org/move/lang/core/types/infer/Acquires.kt @@ -10,6 +10,7 @@ import org.move.cli.MoveProject import org.move.lang.core.psi.MvCallExpr import org.move.lang.core.psi.MvFunction import org.move.lang.core.psi.acquiresPathTypes +import org.move.lang.core.psi.ext.MvCallable import org.move.lang.core.psi.ext.isInline import org.move.lang.core.types.ty.Ty import org.move.lang.core.types.ty.TyFunction @@ -31,7 +32,7 @@ val MoveProject.acquiresContext: AcquiresTypeContext class AcquiresTypeContext { private val functionTypes: MutableMap> = concurrentMapOf() - private val callExprTypes: MutableMap> = concurrentMapOf() + private val callableTypes: MutableMap> = concurrentMapOf() fun getFunctionTypes(function: MvFunction): List { val inference = function.inference(false) @@ -39,7 +40,7 @@ class AcquiresTypeContext { if (function.isInline) { // collect inner callExpr types val allTypes = mutableListOf() - for (innerCallExpr in inference.callExprTypes.keys) { + for (innerCallExpr in inference.callableTypes.keys) { val types = getCallTypes(innerCallExpr, inference) allTypes.addAll(types) } @@ -51,9 +52,9 @@ class AcquiresTypeContext { } } - fun getCallTypes(callExpr: MvCallExpr, inference: InferenceResult): List { - return callExprTypes.getOrPut(callExpr) { - val callTy = inference.getCallExprType(callExpr) as? TyFunction ?: return emptyList() + fun getCallTypes(callable: MvCallable, inference: InferenceResult): List { + return callableTypes.getOrPut(callable) { + val callTy = inference.getCallableType(callable) as? TyFunction ?: return emptyList() val callItem = callTy.item as? MvFunction ?: return emptyList() if (callItem.isInline) { val functionTypes = this.getFunctionTypes(callItem) diff --git a/src/main/kotlin/org/move/lang/core/types/infer/InferenceContext.kt b/src/main/kotlin/org/move/lang/core/types/infer/InferenceContext.kt index 7ad697b17..62e81c706 100644 --- a/src/main/kotlin/org/move/lang/core/types/infer/InferenceContext.kt +++ b/src/main/kotlin/org/move/lang/core/types/infer/InferenceContext.kt @@ -41,6 +41,7 @@ fun compatAbilities(expectedTy: Ty, actualTy: Ty, msl: Boolean): Boolean { } fun isCompatible(expectedTy: Ty, actualTy: Ty, msl: Boolean): Boolean { + // TODO: do we need to skip unification then, if we recreate the InferenceContext anyway? val inferenceCtx = InferenceContext(msl, skipUnification = true) return inferenceCtx.combineTypes(expectedTy, actualTy).isOk } @@ -72,8 +73,10 @@ data class InferenceResult( override val patTypes: Map, private val exprTypes: Map, private val exprExpectedTypes: Map, - val callExprTypes: Map, - private val pathTypes: Map, + private val methodOrPathTypes: Map, + private val resolvedFields: Map, + private val resolvedMethodCalls: Map, + val callableTypes: Map, val typeErrors: List ): InferenceData { fun getExprType(expr: MvExpr): Ty = exprTypes[expr] ?: expr.project.inferenceErrorOrTyUnknown(expr) @@ -86,8 +89,11 @@ data class InferenceResult( fun getExprTypeOrNull(expr: MvExpr): Ty? = exprTypes[expr] fun getExpectedType(expr: MvExpr): Ty = exprExpectedTypes[expr] ?: TyUnknown - fun getCallExprType(expr: MvCallExpr): Ty? = callExprTypes[expr] - fun getPathType(path: MvPath): Ty? = pathTypes[path] + fun getCallableType(callable: MvCallable): Ty? = callableTypes[callable] + fun getMethodOrPathType(methodOrPath: MvMethodOrPath): Ty? = methodOrPathTypes[methodOrPath] + + fun getResolvedField(field: MvStructDotField): MvNamedElement? = resolvedFields[field] + fun getResolvedMethod(methodCall: MvMethodCall): MvNamedElement? = resolvedMethodCalls[methodCall] } fun inferTypesIn(element: MvInferenceContextOwner, msl: Boolean): InferenceResult { @@ -141,8 +147,13 @@ class InferenceContext( private val exprTypes = mutableMapOf() private val exprExpectedTypes = mutableMapOf() - private val callExprTypes = mutableMapOf() - private val pathTypes = mutableMapOf() + private val callableTypes = mutableMapOf() + + // private val pathTypes = mutableMapOf() + private val methodOrPathTypes = mutableMapOf() + + val resolvedFields = mutableMapOf() + val resolvedMethodCalls = mutableMapOf() private val typeErrors = mutableListOf() @@ -154,7 +165,7 @@ class InferenceContext( varUnificationTable.startSnapshot(), ) - inline fun probe(action: () -> T): T { + inline fun freezeUnificationTable(action: () -> T): T { val snapshot = startSnapshot() try { return action() @@ -188,18 +199,21 @@ class InferenceContext( // for call expressions, we need to leave unresolved ty vars intact // to determine whether an explicit type annotation required - callExprTypes.replaceAll { _, ty -> resolveTypeVarsIfPossible(ty) } + callableTypes.replaceAll { _, ty -> resolveTypeVarsIfPossible(ty) } exprExpectedTypes.replaceAll { _, ty -> fullyResolveWithOrigins(ty) } typeErrors.replaceAll { err -> fullyResolveWithOrigins(err) } - pathTypes.replaceAll { _, ty -> fullyResolveWithOrigins(ty) } +// pathTypes.replaceAll { _, ty -> fullyResolveWithOrigins(ty) } + methodOrPathTypes.replaceAll { _, ty -> fullyResolveWithOrigins(ty) } return InferenceResult( patTypes, exprTypes, exprExpectedTypes, - callExprTypes, - pathTypes, + methodOrPathTypes, + resolvedFields, + resolvedMethodCalls, + callableTypes, typeErrors ) } @@ -240,18 +254,20 @@ class InferenceContext( this.exprExpectedTypes[expr] = ty } - fun writeCallExprType(callExpr: MvCallExpr, ty: Ty) { - this.callExprTypes[callExpr] = ty + fun writeCallableType(callable: MvCallable, ty: Ty) { + this.callableTypes[callable] = ty } @Suppress("UNCHECKED_CAST") - fun instantiatePath( - path: MvPath, + fun instantiateMethodOrPath( + methodOrPath: MvMethodOrPath, genericItem: MvTypeParametersOwner ): Pair? { var itemTy = - this.pathTypes.getOrPut(path) { - TyLowering.lowerPath(path, msl) as? T ?: return null + this.methodOrPathTypes.getOrPut(methodOrPath) { + // can only be method or path, both are resolved to MvNamedElement + val genericNamedItem = genericItem as MvNamedElement + TyLowering.lowerPath(methodOrPath, genericNamedItem, msl) as? T ?: return null } val typeParameters = genericItem.tyInfers diff --git a/src/main/kotlin/org/move/lang/core/types/infer/Paths.kt b/src/main/kotlin/org/move/lang/core/types/infer/Paths.kt index 99c7fccde..291727b0b 100644 --- a/src/main/kotlin/org/move/lang/core/types/infer/Paths.kt +++ b/src/main/kotlin/org/move/lang/core/types/infer/Paths.kt @@ -1,14 +1,15 @@ package org.move.lang.core.types.infer import org.move.lang.core.psi.* +import org.move.lang.core.psi.ext.MvMethodOrPath import org.move.lang.core.types.infer.RsPsiSubstitution.Value fun pathPsiSubst( - path: MvPath, + methodOrPath: MvMethodOrPath, resolved: MvTypeParametersOwner, ): RsPsiSubstitution { val typeParameters = resolved.typeParameters - val parent = path.parent + val parent = methodOrPath.parent // Generic arguments are optional in expression context, e.g. // `let a = Foo::::bar::();` can be written as `let a = Foo::bar();` // if it is possible to infer `u8` and `u16` during type inference @@ -16,7 +17,7 @@ fun pathPsiSubst( val isPatPath = parent is MvPat || parent is MvPath && parent.parent is MvPat val areOptionalArgs = isExprPath || isPatPath - val typeArguments = path.typeArgumentList?.typeArgumentList?.map { it.type } + val typeArguments = methodOrPath.typeArgumentList?.typeArgumentList?.map { it.type } val typeSubst = associateSubst(typeParameters, typeArguments, areOptionalArgs) return RsPsiSubstitution(typeSubst) } diff --git a/src/main/kotlin/org/move/lang/core/types/infer/Patterns.kt b/src/main/kotlin/org/move/lang/core/types/infer/Patterns.kt index 2d5a4b6a6..e3f9e7e36 100644 --- a/src/main/kotlin/org/move/lang/core/types/infer/Patterns.kt +++ b/src/main/kotlin/org/move/lang/core/types/infer/Patterns.kt @@ -73,7 +73,7 @@ fun MvPat.collectBindings(fctx: TypeInferenceWalker, ty: Ty) { } is MvStructPat -> { val structItem = this.structItem ?: (ty as? TyStruct)?.item ?: return - val (patTy, _) = fctx.ctx.instantiatePath(this.path, structItem) ?: return + val (patTy, _) = fctx.ctx.instantiateMethodOrPath(this.path, structItem) ?: return if (!isCompatible(ty.derefIfNeeded(), patTy, fctx.msl)) { fctx.reportTypeError(TypeError.InvalidUnpacking(this, ty)) } diff --git a/src/main/kotlin/org/move/lang/core/types/infer/TyLowering.kt b/src/main/kotlin/org/move/lang/core/types/infer/TyLowering.kt index 8bd5fdebb..708b831e3 100644 --- a/src/main/kotlin/org/move/lang/core/types/infer/TyLowering.kt +++ b/src/main/kotlin/org/move/lang/core/types/infer/TyLowering.kt @@ -12,7 +12,10 @@ fun MvType.loweredType(msl: Boolean): Ty = TyLowering.lowerType(this, msl) class TyLowering { fun lowerTy(moveType: MvType, msl: Boolean): Ty { return when (moveType) { - is MvPathType -> lowerPath(moveType.path, msl) + is MvPathType -> { + val genericItem = moveType.path.reference?.resolveWithAliases() + lowerPath(moveType.path, genericItem, msl) + } is MvRefType -> { val mutabilities = RefPermissions.valueOf(moveType.mutable) val refInnerType = moveType.type @@ -43,16 +46,18 @@ class TyLowering { } } - private fun lowerPath(path: MvPath, msl: Boolean): Ty { - val namedItem = path.reference?.resolveWithAliases() + private fun lowerPath(methodOrPath: MvMethodOrPath, namedItem: MvNamedElement?, msl: Boolean): Ty { + // cannot do resolve() here due to circular caching for MethodCall, need to pass namedItem explicitly, + // namedItem can be null if it's a primitive type +// val namedItem = methodOrPath.reference?.resolveWithAliases() if (namedItem == null) { - return lowerPrimitiveTy(path, msl) + return if (methodOrPath is MvPath) lowerPrimitiveTy(methodOrPath, msl) else TyUnknown } return when (namedItem) { is MvTypeParameter -> TyTypeParameter(namedItem) is MvTypeParametersOwner -> { val baseTy = namedItem.declaredType(msl) - val explicitSubst = instantiateTypeParamsSubstitution(path, namedItem, msl) + val explicitSubst = instantiateTypeParamsSubstitution(methodOrPath, namedItem, msl) // val (_, explicits) = instantiatePathGenerics(path, namedItem, msl) baseTy.substitute(explicitSubst) } @@ -84,13 +89,13 @@ class TyLowering { } private fun instantiateTypeParamsSubstitution( - path: MvPath, + methodOrPath: MvMethodOrPath, namedItem: T, msl: Boolean ): Substitution { if (namedItem !is MvTypeParametersOwner) return emptySubstitution - val psiSubstitution = pathPsiSubst(path, namedItem) + val psiSubstitution = pathPsiSubst(methodOrPath, namedItem) val typeSubst = hashMapOf() for ((param, value) in psiSubstitution.typeSubst.entries) { @@ -110,8 +115,8 @@ class TyLowering { return TyLowering().lowerTy(type, msl) } - fun lowerPath(path: MvPath, msl: Boolean): Ty { - return TyLowering().lowerPath(path, msl) + fun lowerPath(path: MvMethodOrPath, namedItem: MvNamedElement?, msl: Boolean): Ty { + return TyLowering().lowerPath(path, namedItem, msl) } } } diff --git a/src/main/kotlin/org/move/lang/core/types/infer/TypeInferenceWalker.kt b/src/main/kotlin/org/move/lang/core/types/infer/TypeInferenceWalker.kt index d2ae1f634..7e6832683 100644 --- a/src/main/kotlin/org/move/lang/core/types/infer/TypeInferenceWalker.kt +++ b/src/main/kotlin/org/move/lang/core/types/infer/TypeInferenceWalker.kt @@ -9,6 +9,7 @@ import org.move.ide.formatter.impl.location import org.move.lang.core.psi.* import org.move.lang.core.psi.ext.* import org.move.lang.core.types.ty.* +import org.move.lang.core.types.ty.TyReference.Companion.autoborrow import org.move.stdext.RsResult import org.move.stdext.chain @@ -109,7 +110,7 @@ class TypeInferenceWalker( } } - private fun resolveTypeVarsWithObligations(ty: Ty): Ty { + fun resolveTypeVarsWithObligations(ty: Ty): Ty { if (!ty.hasTyInfer) return ty val tyRes = ctx.resolveTypeVarsIfPossible(ty) if (!tyRes.hasTyInfer) return tyRes @@ -210,7 +211,7 @@ class TypeInferenceWalker( is MvVectorLitExpr -> inferVectorLitExpr(expr, expected) is MvIndexExpr -> inferIndexExprTy(expr) - is MvDotExpr -> inferDotExprTy(expr) + is MvDotExpr -> inferDotExprTy(expr, expected) is MvDerefExpr -> inferDerefExprTy(expr) is MvLitExpr -> inferLitExprTy(expr, expected) is MvTupleLitExpr -> inferTupleLitExprTy(expr, expected) @@ -348,7 +349,7 @@ class TypeInferenceWalker( val baseTy = when (item) { is MvFunctionLike -> { - val (itemTy, _) = ctx.instantiatePath(path, item) ?: return TyUnknown + val (itemTy, _) = ctx.instantiateMethodOrPath(path, item) ?: return TyUnknown itemTy } is MvBindingPat -> { @@ -362,12 +363,24 @@ class TypeInferenceWalker( val expectedInputTys = expectedInputsForExpectedOutput(expected, funcTy.retType, funcTy.paramTypes) - inferArgumentTypes(funcTy.paramTypes, expectedInputTys, callExpr.callArgumentExprs) + inferArgumentTypes( + funcTy.paramTypes, + expectedInputTys, + callExpr.argumentExprs.map { InferArg.ArgExpr(it) }) - // if value parameter has no type, use it as unknown for the sake of "need type annotation" check - ctx.probe { - val valueArguments = callExpr.valueArguments - for ((i, paramType) in funcTy.paramTypes.withIndex()) { + writeCallableType(callExpr, funcTy, method = false) + + return funcTy.retType + } + + private fun writeCallableType(callable: MvCallable, funcTy: TyCallable, method: Boolean) { + // callableType TyVar are meaningful mostly for "needs type annotation" error. + // if value parameter is missing, we don't want to show that error, so we cover + // unknown parameters with TyUnknown here + ctx.freezeUnificationTable { + val valueArguments = callable.valueArguments + val paramTypes = funcTy.paramTypes.drop(if (method) 1 else 0) + for ((i, paramType) in paramTypes.withIndex()) { val argumentExpr = valueArguments.getOrNull(i)?.expr if (argumentExpr == null) { paramType.visitInferTys { @@ -375,31 +388,114 @@ class TypeInferenceWalker( } } } - ctx.writeCallExprType(callExpr, ctx.resolveTypeVarsIfPossible(funcTy as Ty)) + ctx.writeCallableType(callable, ctx.resolveTypeVarsIfPossible(funcTy as Ty)) } + } - return funcTy.retType + fun inferDotFieldTy(receiverTy: Ty, dotField: MvStructDotField, expected: Expectation): Ty { + val structTy = + receiverTy.derefIfNeeded() as? TyStruct ?: return TyUnknown + + val field = + getFieldVariants(dotField, structTy, msl).filterByName(dotField.referenceName).singleOrNull() + ctx.resolvedFields[dotField] = field + + val fieldTy = field?.type?.loweredType(msl)?.substitute(structTy.typeParameterValues) + return fieldTy ?: TyUnknown + } + + fun inferMethodCallTy(receiverTy: Ty, methodCall: MvMethodCall, expected: Expectation): Ty { + val refName = methodCall.referenceName + val methodVariants = getMethodVariants(methodCall, receiverTy, ctx.msl) + val genericItem = methodVariants.filterByName(refName).firstOrNull() + + ctx.resolvedMethodCalls[methodCall] = genericItem + + val baseTy = + when (genericItem) { + is MvFunction -> { + val (itemTy, _) = + ctx.instantiateMethodOrPath(methodCall, genericItem) ?: return TyUnknown + itemTy + } + else -> { + // 1 for `self` + TyFunction.unknownTyFunction(methodCall.project, 1 + methodCall.valueArguments.size) + } + } + val methodTy = ctx.resolveTypeVarsIfPossible(baseTy) as TyCallable + + val expectedInputTys = + expectedInputsForExpectedOutput(expected, methodTy.retType, methodTy.paramTypes) + + inferArgumentTypes( + methodTy.paramTypes, + expectedInputTys, + listOf(InferArg.SelfType(receiverTy)) + + methodCall.argumentExprs.map { InferArg.ArgExpr(it) } + ) + + writeCallableType(methodCall, methodTy, method = true) + + return methodTy.retType + } + + sealed class InferArg { + data class SelfType(val selfTy: Ty): InferArg() + data class ArgExpr(val expr: MvExpr?): InferArg() } private fun inferArgumentTypes( formalInputTys: List, expectedInputTys: List, - argExprs: List + inferArgs: List, ) { - for ((i, argExpr) in argExprs.withIndex()) { - if (argExpr == null) continue - + for ((i, inferArg) in inferArgs.withIndex()) { val formalInputTy = formalInputTys.getOrNull(i) ?: TyUnknown val expectedInputTy = expectedInputTys.getOrNull(i) ?: formalInputTy - val expectation = Expectation.maybeHasType(expectedInputTy) - val actualTy = inferExprTy(argExpr, expectation) - val coercedTy = + val expectedTy = resolveTypeVarsWithObligations(expectation.onlyHasTy(ctx) ?: formalInputTy) - coerce(argExpr, actualTy, coercedTy) + when (inferArg) { + is InferArg.ArgExpr -> { + val argExpr = inferArg.expr ?: continue + val argExprTy = inferExprTy(argExpr, expectation) +// val coercedTy = +// resolveTypeVarsWithObligations(expectation.onlyHasTy(ctx) ?: formalInputTy) + coerce(argExpr, argExprTy, expectedTy) + // retrieve obligations + ctx.combineTypes(formalInputTy, expectedTy) +// coercedTy + } + is InferArg.SelfType -> { +// val actualSelfTy = inferArg.selfTy +// val coercedTy = +// resolveTypeVarsWithObligations(expectation.onlyHasTy(ctx) ?: formalInputTy) +// ctx.combineTypes( +// resolveTypeVarsWithObligations(actualTy), +// resolveTypeVarsWithObligations(expectedTy) +// ) + + // method already resolved, so autoborrow() should always succeed + val actualSelfTy = autoborrow(inferArg.selfTy, expectedTy) + ?: error("unreachable, as method call cannot be resolved if autoborrow fails") + ctx.combineTypes(actualSelfTy, expectedTy) +// coercedTy + + } + } // retrieve obligations - ctx.combineTypes(formalInputTy, coercedTy) + ctx.combineTypes(formalInputTy, expectedTy) +// if (inferArg == null) continue + +// val actualTy = inferExprTy(inferArg, expectation) +// val coercedTy = +// resolveTypeVarsWithObligations(expectation.onlyHasTy(ctx) ?: formalInputTy) +// coerce(inferArg, actualTy, coercedTy) +// +// // retrieve obligations +// ctx.combineTypes(formalInputTy, coercedTy) } } @@ -407,7 +503,11 @@ class TypeInferenceWalker( val ident = macroExpr.identifier if (ident.text == "assert") { val formalInputTys = listOf(TyBool, TyInteger.default()) - inferArgumentTypes(formalInputTys, emptyList(), macroExpr.valueArguments.map { it.expr }) + inferArgumentTypes( + formalInputTys, + emptyList(), + macroExpr.valueArguments.map { it.expr }.map { InferArg.ArgExpr(it) } + ) } return TyUnit } @@ -426,7 +526,7 @@ class TypeInferenceWalker( // Rustc does `fudge` instead of `probe` here. But `fudge` seems useless in our simplified type inference // because we don't produce new type variables during unification // https://github.com/rust-lang/rust/blob/50cf76c24bf6f266ca6d253a/compiler/rustc_infer/src/infer/fudge.rs#L98 - return ctx.probe { + return ctx.freezeUnificationTable { if (ctx.combineTypes(retTy, resolvedFormalRet).isOk) { formalArgs.map { ctx.resolveTypeVarsIfPossible(it) } } else { @@ -456,21 +556,17 @@ class TypeInferenceWalker( } } - private fun inferDotExprTy(dotExpr: MvDotExpr): Ty { - val baseTy = ctx.resolveTypeVarsIfPossible(dotExpr.expr.inferType()) - val structTy = when (baseTy) { - is TyReference -> baseTy.innermostTy() as? TyStruct - is TyStruct -> baseTy - else -> null - } ?: return TyUnknown + private fun inferDotExprTy(dotExpr: MvDotExpr, expected: Expectation): Ty { + val receiverTy = ctx.resolveTypeVarsIfPossible(dotExpr.expr.inferType()) - val item = structTy.item - val fieldName = dotExpr.structDotField?.referenceName ?: return TyUnknown - val fieldTy = item.fieldsMap[fieldName] - ?.type - ?.loweredType(msl) - ?.substitute(structTy.typeParameterValues) - return fieldTy ?: TyUnknown + val methodCall = dotExpr.methodCall + val field = dotExpr.structDotField + return when { + methodCall != null -> inferMethodCallTy(receiverTy, methodCall, expected) + field != null -> inferDotFieldTy(receiverTy, field, expected) + // incomplete + else -> TyUnknown + } } fun inferStructLitExprTy(litExpr: MvStructLitExpr, expected: Expectation): Ty { @@ -483,7 +579,8 @@ class TypeInferenceWalker( return TyUnknown } - val (structTy, typeParameters) = ctx.instantiatePath(path, structItem) ?: return TyUnknown + val (structTy, typeParameters) = ctx.instantiateMethodOrPath(path, structItem) + ?: return TyUnknown expected.onlyHasTy(ctx)?.let { expectedTy -> ctx.unifySubst(typeParameters, expectedTy.typeParameterValues) } @@ -512,7 +609,7 @@ class TypeInferenceWalker( return TyUnknown } - val (schemaTy, _) = ctx.instantiatePath(path, schemaItem) ?: return TyUnknown + val (schemaTy, _) = ctx.instantiateMethodOrPath(path, schemaItem) ?: return TyUnknown // expected.onlyHasTy(ctx)?.let { expectedTy -> // ctx.unifySubst(typeParameters, expectedTy.typeParameterValues) // } @@ -548,7 +645,11 @@ class TypeInferenceWalker( val formalInputs = generateSequence { ctx.resolveTypeVarsIfPossible(tyVar) }.take(exprs.size).toList() val expectedInputTys = expectedInputsForExpectedOutput(expected, TyVector(tyVar), formalInputs) - inferArgumentTypes(formalInputs, expectedInputTys, exprs) + inferArgumentTypes( + formalInputs, + expectedInputTys, + exprs.map { InferArg.ArgExpr(it) } + ) val vectorTy = ctx.resolveTypeVarsIfPossible(TyVector(tyVar)) return vectorTy diff --git a/src/main/kotlin/org/move/lang/core/types/ty/Ty.kt b/src/main/kotlin/org/move/lang/core/types/ty/Ty.kt index 8be96abec..44e8d77e2 100644 --- a/src/main/kotlin/org/move/lang/core/types/ty/Ty.kt +++ b/src/main/kotlin/org/move/lang/core/types/ty/Ty.kt @@ -40,6 +40,8 @@ val TypeFoldable<*>.hasTyUnknown get() = visitWith(HAS_TY_UNKNOWN_VISITOR) val TypeFoldable<*>.needsInfer get(): Boolean = visitWith(NEEDS_INFER) val TypeFoldable<*>.needsSubst get(): Boolean = visitWith(NEEDS_SUBST) +fun Ty.knownOrNull(): Ty? = takeIf { it !is TyUnknown } + abstract class Ty(val flags: TypeFlags = 0) : TypeFoldable { override fun foldWith(folder: TypeFolder): Ty = folder(this) diff --git a/src/main/kotlin/org/move/lang/core/types/ty/TyReference.kt b/src/main/kotlin/org/move/lang/core/types/ty/TyReference.kt index b96eb6f34..150450173 100644 --- a/src/main/kotlin/org/move/lang/core/types/ty/TyReference.kt +++ b/src/main/kotlin/org/move/lang/core/types/ty/TyReference.kt @@ -6,9 +6,11 @@ package org.move.lang.core.types.ty import org.move.ide.presentation.tyToString - import org.move.lang.core.types.infer.TypeFolder import org.move.lang.core.types.infer.TypeVisitor +import org.move.lang.core.types.infer.isCompatible +import org.move.lang.core.types.ty.RefPermissions.READ +import org.move.lang.core.types.ty.RefPermissions.WRITE enum class RefPermissions { READ, @@ -24,10 +26,10 @@ data class TyReference( val referenced: Ty, val permissions: Set, val msl: Boolean -) : Ty(referenced.flags) { +): Ty(referenced.flags) { override fun abilities() = setOf(Ability.COPY, Ability.DROP) - val isMut: Boolean get() = this.permissions.contains(RefPermissions.WRITE) + val isMut: Boolean get() = this.permissions.contains(WRITE) fun innerTy(): Ty { return if (referenced is TyReference) { @@ -56,10 +58,32 @@ data class TyReference( override fun toString(): String = tyToString(this) companion object { - fun ref(ty: Ty, msl: Boolean): TyReference = TyReference(ty, setOf(RefPermissions.READ), msl) + fun ref(ty: Ty, mut: Boolean, msl: Boolean = false): TyReference = + TyReference(ty, if (mut) setOf(READ, WRITE) else setOf(READ), msl) fun coerceMutability(inferred: TyReference, expected: TyReference): Boolean { return inferred.isMut || !expected.isMut } + + fun isCompatibleWithAutoborrow(ty: Ty, intoTy: Ty, msl: Boolean): Boolean { + // if underlying types are different, no match + val autoborrowedTy = autoborrow(ty, intoTy) ?: return false + return isCompatible(intoTy, autoborrowedTy, msl) + } + + fun autoborrow(ty: Ty, intoTy: Ty): Ty? { + return if (intoTy is TyReference) coerceAutoborrow(ty, intoTy.isMut) else ty + } + + fun coerceAutoborrow(ty: Ty, mut: Boolean): Ty? { + return when { + ty !is TyReference -> ref(ty, mut) + mut && ty.isMut -> ty + mut && !ty.isMut -> null + !mut && ty.isMut -> ref(ty.innerTy(), false) + !mut && !ty.isMut -> ty + else -> null + } + } } } diff --git a/src/main/kotlin/org/move/lang/core/types/ty/TyTypeParameter.kt b/src/main/kotlin/org/move/lang/core/types/ty/TyTypeParameter.kt index 06fd51e69..66780c919 100644 --- a/src/main/kotlin/org/move/lang/core/types/ty/TyTypeParameter.kt +++ b/src/main/kotlin/org/move/lang/core/types/ty/TyTypeParameter.kt @@ -22,5 +22,8 @@ data class TyTypeParameter(val origin: MvTypeParameter) : Ty(HAS_TY_TYPE_PARAMET return parentStruct?.requiredAbilitiesForTypeParam.orEmpty() } + override fun equals(other: Any?): Boolean = other is TyTypeParameter && other.origin == origin + override fun hashCode(): Int = origin.hashCode() + override fun toString(): String = tyToString(this) } diff --git a/src/main/kotlin/org/move/lang/index/MvNamedElementIndex.kt b/src/main/kotlin/org/move/lang/index/MvNamedElementIndex.kt index aea913305..42c37e758 100644 --- a/src/main/kotlin/org/move/lang/index/MvNamedElementIndex.kt +++ b/src/main/kotlin/org/move/lang/index/MvNamedElementIndex.kt @@ -1,14 +1,15 @@ package org.move.lang.index import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.stubs.StringStubIndexExtension import com.intellij.psi.stubs.StubIndex import com.intellij.psi.stubs.StubIndexKey +import org.move.lang.core.psi.MvModule import org.move.lang.core.psi.MvNamedElement import org.move.lang.core.stubs.impl.MvFileStub import org.move.openapiext.checkCommitIsNotInProgress -import org.move.openapiext.getElements class MvNamedElementIndex : StringStubIndexExtension() { override fun getVersion(): Int = MvFileStub.Type.stubVersion @@ -33,5 +34,14 @@ class MvNamedElementIndex : StringStubIndexExtension() { StubIndex.getInstance() .processElements(KEY, target, project, scope, MvNamedElement::class.java, processor) } + + fun getElementsByName( + project: Project, + name: String, + scope: GlobalSearchScope, + ): Collection { + checkCommitIsNotInProgress(project) + return StubIndex.getElements(KEY, name, project, scope, MvNamedElement::class.java) + } } } diff --git a/src/main/kotlin/org/move/openapiext/Project.kt b/src/main/kotlin/org/move/openapiext/Project.kt index fe9a53052..dcc9a3241 100644 --- a/src/main/kotlin/org/move/openapiext/Project.kt +++ b/src/main/kotlin/org/move/openapiext/Project.kt @@ -24,7 +24,7 @@ fun Project.aptosCommandConfigurationsSettings(): List = // aptosCommandConfigurations().filter { it.command.startsWith("move compile") } -inline fun Project.showSettings() { +inline fun Project.showSettingsDialog() { ShowSettingsUtil.getInstance().showSettingsDialog(this, T::class.java) } diff --git a/src/main/kotlin/org/move/openapiext/utils.kt b/src/main/kotlin/org/move/openapiext/utils.kt index 5e792e72a..efbd51083 100644 --- a/src/main/kotlin/org/move/openapiext/utils.kt +++ b/src/main/kotlin/org/move/openapiext/utils.kt @@ -5,6 +5,7 @@ package org.move.openapiext +import com.intellij.execution.Platform import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.DataContext @@ -121,12 +122,13 @@ val Project.contentRoots: Sequence get() = this.modules.asSequence() .flatMap { ModuleRootManager.getInstance(it).contentRoots.asSequence() } -val Project.syntheticLibraries: Collection get() { - val libraries = AdditionalLibraryRootsProvider.EP_NAME - .extensionList - .flatMap { it.getAdditionalProjectLibraries(this) } - return libraries -} +val Project.syntheticLibraries: Collection + get() { + val libraries = AdditionalLibraryRootsProvider.EP_NAME + .extensionList + .flatMap { it.getAdditionalProjectLibraries(this) } + return libraries + } val Project.rootDir: VirtualFile? get() = contentRoots.firstOrNull() @@ -188,7 +190,7 @@ inline fun Project.nonBlocking(crossinline block: () -> R, crossinline uiCon } @Service -class MvPluginDisposable : Disposable { +class MvPluginDisposable: Disposable { companion object { @JvmStatic fun getInstance(project: Project): Disposable = project.service() @@ -206,10 +208,13 @@ fun checkCommitIsNotInProgress(project: Project) { } } -inline fun getElements( +inline fun getElements( indexKey: StubIndexKey, key: Key, project: Project, scope: GlobalSearchScope? ): Collection = StubIndex.getElements(indexKey, key, project, scope, Psi::class.java) + +fun joinPath(segments: Array) = + segments.joinTo(StringBuilder(), Platform.current().fileSeparator.toString()).toString() \ No newline at end of file diff --git a/src/main/kotlin/org/move/stdext/Paths.kt b/src/main/kotlin/org/move/stdext/Paths.kt index f03ba85d6..fc18cbec8 100644 --- a/src/main/kotlin/org/move/stdext/Paths.kt +++ b/src/main/kotlin/org/move/stdext/Paths.kt @@ -33,3 +33,5 @@ fun Path.exists(): Boolean = Files.exists(this) fun executableName(toolName: String): String = if (SystemInfo.isWindows) "$toolName.exe" else toolName + +fun String.blankToNull(): String? = ifBlank { null } \ No newline at end of file diff --git a/src/main/kotlin/org/move/utils/EnvUtils.kt b/src/main/kotlin/org/move/utils/EnvUtils.kt new file mode 100644 index 000000000..02264a9e5 --- /dev/null +++ b/src/main/kotlin/org/move/utils/EnvUtils.kt @@ -0,0 +1,23 @@ +package org.move.utils + +import org.move.stdext.exists +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +object EnvUtils { + fun findInPATH(binaryName: String): Path? { + val dirPaths = getEnv("PATH")?.split(":").orEmpty() + for (dirPath in dirPaths) { + val path = Paths.get(dirPath, binaryName) + if (path.exists()) { + return path + } + } + return null + } + + fun getEnv(name: String): String? = System.getenv(name) +} + diff --git a/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt b/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt index 618a1c841..361240e10 100644 --- a/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt +++ b/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt @@ -10,6 +10,7 @@ import com.intellij.psi.impl.PsiManagerEx import com.intellij.testFramework.builders.ModuleFixtureBuilder import com.intellij.testFramework.fixtures.CodeInsightFixtureTestCase import com.intellij.testFramework.fixtures.CodeInsightTestFixture +import com.intellij.util.ui.UIUtil import org.intellij.lang.annotations.Language import org.move.cli.moveProjectsService import org.move.cli.settings.Blockchain @@ -25,15 +26,13 @@ abstract class MvProjectTestBase : CodeInsightFixtureTestCase()?.enabled ?: true val blockchain = this.findAnnotationInstance()?.blockchain ?: Blockchain.APTOS // triggers projects refresh - project.moveSettings.state = settingsState.copy( - debugMode = debugMode, - blockchain = blockchain - ) + project.moveSettings.modify { + it.debugMode = debugMode + it.blockchain = blockchain + } } override fun tearDown() { @@ -102,4 +101,24 @@ abstract class MvProjectTestBase : CodeInsightFixtureTestCase Boolean + ) { + repeat(retries) { + UIUtil.dispatchAllInvocationEvents() + if (action()) { + return + } + Thread.sleep(10) + } + error(errorMessage) + } } diff --git a/src/main/kotlin/org/move/utils/tests/MvTestBase.kt b/src/main/kotlin/org/move/utils/tests/MvTestBase.kt index ae6867b17..97c64048b 100644 --- a/src/main/kotlin/org/move/utils/tests/MvTestBase.kt +++ b/src/main/kotlin/org/move/utils/tests/MvTestBase.kt @@ -6,6 +6,7 @@ package org.move.utils.tests import com.intellij.codeInspection.InspectionProfileEntry +import com.intellij.psi.PsiElement import com.intellij.testFramework.enableInspectionTool import com.intellij.testFramework.fixtures.BasePlatformTestCase import org.intellij.lang.annotations.Language @@ -13,6 +14,7 @@ import org.move.cli.settings.Blockchain import org.move.cli.settings.moveSettings import org.move.utils.tests.base.MvTestCase import org.move.utils.tests.base.TestCase +import org.move.utils.tests.base.findElementsWithDataAndOffsetInEditor import java.lang.annotation.Inherited import kotlin.reflect.KClass import kotlin.reflect.full.createInstance @@ -43,15 +45,13 @@ abstract class MvTestBase: BasePlatformTestCase(), setupInspections() - val settingsState = project.moveSettings.state - val debugMode = this.findAnnotationInstance()?.enabled ?: true val blockchain = this.findAnnotationInstance()?.blockchain ?: Blockchain.APTOS // triggers projects refresh - project.moveSettings.state = settingsState.copy( - debugMode = debugMode, - blockchain = blockchain - ) + project.moveSettings.modify { + it.debugMode = debugMode + it.blockchain = blockchain + } } private fun setupInspections() { @@ -66,16 +66,51 @@ abstract class MvTestBase: BasePlatformTestCase(), return TestCase.camelOrWordsToSnake(camelCase) } - protected fun inlineFile(@Language("Move") code: String, name: String = "main.move"): InlineFile { + protected fun InlineFile(@Language("Move") code: String, name: String = "main.move"): InlineFile { return InlineFile(myFixture, code, name) } + protected inline fun findElementInEditor(marker: String = "^"): T = + findElementInEditor(T::class.java, marker) + + protected fun findElementInEditor(psiClass: Class, marker: String): T { + val (element, data) = findElementWithDataAndOffsetInEditor(psiClass, marker) + check(data.isEmpty()) { "Did not expect marker data" } + return element + } + + protected inline fun findElementAndDataInEditor(marker: String = "^"): Pair { + val (element, data) = findElementWithDataAndOffsetInEditor(marker) + return element to data + } + + protected inline fun findElementAndOffsetInEditor(marker: String = "^"): Pair { + val (element, _, offset) = findElementWithDataAndOffsetInEditor(marker) + return element to offset + } + + protected inline fun findElementWithDataAndOffsetInEditor( + marker: String = "^" + ): Triple { + return findElementWithDataAndOffsetInEditor(T::class.java, marker) + } + + protected fun findElementWithDataAndOffsetInEditor( + psiClass: Class, + marker: String + ): Triple { + val elementsWithDataAndOffset = myFixture.findElementsWithDataAndOffsetInEditor(psiClass, marker) + check(elementsWithDataAndOffset.isNotEmpty()) { "No `$marker` marker:\n${myFixture.file.text}" } + check(elementsWithDataAndOffset.size <= 1) { "More than one `$marker` marker:\n${myFixture.file.text}" } + return elementsWithDataAndOffset.first() + } + protected fun checkByText( @Language("Move") before: String, @Language("Move") after: String, action: () -> Unit, ) { - inlineFile(before) + InlineFile(before) action() myFixture.checkResult(replaceCaretMarker(after)) } diff --git a/src/main/kotlin/org/move/utils/tests/RunConfigurationUtils.kt b/src/main/kotlin/org/move/utils/tests/RunConfigurationUtils.kt new file mode 100644 index 000000000..11bd3e18d --- /dev/null +++ b/src/main/kotlin/org/move/utils/tests/RunConfigurationUtils.kt @@ -0,0 +1,111 @@ +package org.move.utils.tests + +import com.intellij.execution.ExecutionListener +import com.intellij.execution.configurations.RunConfiguration +import com.intellij.execution.impl.ExecutionManagerImpl +import com.intellij.execution.process.* +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.ui.RunContentDescriptor +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.Key +import com.intellij.testFramework.PlatformTestUtil +import com.intellij.util.ConcurrencyUtil +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@Throws(InterruptedException::class) +fun CountDownLatch.waitFinished(timeoutMs: Long): Boolean { + for (i in 1..timeoutMs / ConcurrencyUtil.DEFAULT_TIMEOUT_MS) { + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() + if (await(ConcurrencyUtil.DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) return true + } + return false +} + +/** + * Capturing adapter that removes ANSI escape codes from the output + */ +class AnsiAwareCapturingProcessAdapter : ProcessAdapter(), AnsiEscapeDecoder.ColoredTextAcceptor { + val output = ProcessOutput() + + private val decoder = object : AnsiEscapeDecoder() { + override fun getCurrentOutputAttributes(outputType: Key<*>) = outputType + } + + override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) = + decoder.escapeText(event.text, outputType, this) + + private fun addToOutput(text: String, outputType: Key<*>) { + if (outputType === ProcessOutputTypes.STDERR) { + output.appendStderr(text) + } else { + output.appendStdout(text) + } + } + + override fun processTerminated(event: ProcessEvent) { + output.exitCode = event.exitCode + } + + override fun coloredTextAvailable(text: String, attributes: Key<*>) = + addToOutput(text, attributes) +} + +class TestExecutionListener( + private val parentDisposable: Disposable, + private val configuration: RunConfiguration +) : ExecutionListener { + + private val latch = CountDownLatch(1) + private val processListener = AnsiAwareCapturingProcessAdapter() + + private var isProcessStarted = false + private var notStartedCause: Throwable? = null + private var descriptor: RunContentDescriptor? = null + + override fun processNotStarted(executorId: String, env: ExecutionEnvironment, cause: Throwable?) { + checkAndExecute(env) { + notStartedCause = cause + latch.countDown() + } + } + + override fun processStarting(executorId: String, env: ExecutionEnvironment, handler: ProcessHandler) { + checkAndExecute(env) { + isProcessStarted = true + handler.addProcessListener(processListener) + Disposer.register(parentDisposable) { + ExecutionManagerImpl.stopProcess(handler) + } + } + } + + override fun processTerminated(executorId: String, env: ExecutionEnvironment, handler: ProcessHandler, exitCode: Int) { + checkAndExecute(env) { + descriptor = env.contentToReuse?.also { + Disposer.register(parentDisposable, it) + } + latch.countDown() + } + } + + private fun checkAndExecute(env: ExecutionEnvironment, action: () -> Unit) { + if (env.runProfile == configuration) { + action() + } + } + + @Throws(InterruptedException::class) + fun waitFinished(timeoutMs: Long = 5000): TestExecutionResult? { + if (!latch.waitFinished(timeoutMs)) return null + val output = if (isProcessStarted) processListener.output else null + return TestExecutionResult(output, descriptor, notStartedCause) + } +} + +data class TestExecutionResult( + val output: ProcessOutput?, + val descriptor: RunContentDescriptor?, + val notStartedCause: Throwable? +) diff --git a/src/main/kotlin/org/move/utils/tests/resolve/ResolveTestCase.kt b/src/main/kotlin/org/move/utils/tests/resolve/ResolveTestCase.kt index 7cbdd9682..5eb31cc40 100644 --- a/src/main/kotlin/org/move/utils/tests/resolve/ResolveTestCase.kt +++ b/src/main/kotlin/org/move/utils/tests/resolve/ResolveTestCase.kt @@ -11,7 +11,7 @@ abstract class ResolveTestCase : MvTestBase() { protected fun checkByCode( @Language("Move") code: String, ) { - inlineFile(code, "main.move") + InlineFile(code, "main.move") val (refElement, data, offset) = myFixture.findElementWithDataAndOffsetInEditor("^") diff --git a/src/main/resources/META-INF/intellij-move-core.xml b/src/main/resources/META-INF/intellij-move-core.xml index ff5f753bd..b132a5660 100644 --- a/src/main/resources/META-INF/intellij-move-core.xml +++ b/src/main/resources/META-INF/intellij-move-core.xml @@ -32,6 +32,8 @@ + + + @@ -281,9 +287,9 @@ - - + implementation="org.move.ide.newProject.projectActivity.AlwaysRefreshProjectsAfterOpen" /> + @@ -301,7 +307,7 @@ - + @@ -356,7 +362,7 @@ @@ -372,9 +378,21 @@ text="Update Move Projects" class="org.move.ide.actions.RefreshMoveProjectsAction" icon="AllIcons.Actions.Refresh" /> + + + + + +
diff --git a/src/test/kotlin/org/move/cli/projectAware/ImportProjectTest.kt b/src/test/kotlin/org/move/cli/externalSystem/ImportProjectTest.kt similarity index 92% rename from src/test/kotlin/org/move/cli/projectAware/ImportProjectTest.kt rename to src/test/kotlin/org/move/cli/externalSystem/ImportProjectTest.kt index f225040c2..c92e79cb1 100644 --- a/src/test/kotlin/org/move/cli/projectAware/ImportProjectTest.kt +++ b/src/test/kotlin/org/move/cli/externalSystem/ImportProjectTest.kt @@ -1,4 +1,4 @@ -package org.move.cli.projectAware +package org.move.cli.externalSystem import org.move.utils.tests.MvProjectTestBase diff --git a/src/test/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAwareTest.kt b/src/test/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAwareTest.kt new file mode 100644 index 000000000..e2c5d2fbf --- /dev/null +++ b/src/test/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAwareTest.kt @@ -0,0 +1,231 @@ +package org.move.cli.externalSystem + +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.externalSystem.autoimport.AutoImportProjectNotificationAware +import com.intellij.openapi.externalSystem.autoimport.AutoImportProjectTracker +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectId +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDocumentManager +import com.intellij.util.PathUtil +import org.move.cli.MoveProjectsService +import org.move.lang.core.psi.MvPath +import org.move.utils.tests.MvProjectTestBase +import org.move.utils.tests.TestProject +import org.move.utils.tests.waitFinished +import java.io.IOException +import java.util.concurrent.CountDownLatch + +@Suppress("UnstableApiUsage") +class MoveExternalSystemProjectAwareTest: MvProjectTestBase() { + + private val notificationAware get() = AutoImportProjectNotificationAware.getInstance(project) + private val projectTracker get() = AutoImportProjectTracker.getInstance(project) + + private val moveSystemId: ExternalSystemProjectId + get() = projectTracker + .getActivatedProjects() + .first { it.systemId == MoveExternalSystemProjectAware.MOVE_SYSTEM_ID } + + override fun setUp() { + super.setUp() + AutoImportProjectTracker.enableAutoReloadInTests(testRootDisposable) + } + + fun `test modifications`() { + val testProject = testProject { + namedMoveToml("RootPackage") + build { + dir("RootPackage") { + buildInfoYaml(""" +--- +compiled_package_info: + package_name: RootPackage +""") + } + } + sources { + main( + """ + module 0x1::main { /*caret*/ } + """ + ) + } + dir("child1") { + namedMoveToml("ChildPackage1") + sources { + main( + """ + module 0x1::main {} + """ + ) + } + } + dir("child2") { + namedMoveToml("ChildPackage2") + sources { + main( + """ + module 0x1::main {} + """ + ) + } + } + } + + assertNotificationAware(event = "after project creation") + + testProject.checkFileModification("Move.toml", triggered = true) + testProject.checkFileModification("build/RootPackage/BuildInfo.yaml", triggered = true) + testProject.checkFileModification("child1/Move.toml", triggered = true) + testProject.checkFileModification("child2/Move.toml", triggered = true) + + testProject.checkFileModification("sources/main.move", triggered = false) + testProject.checkFileModification("child1/sources/main.move", triggered = false) + testProject.checkFileModification("child2/sources/main.move", triggered = false) + } + + fun `test reload`() { + val testProject = testProject { + moveToml(""" + [package] + name = "MainPackage" + + [dependencies] + #Dep = { local = "./dep" } + """) + sources { + main(""" + module 0x1::main { + fun main() { + 0x1::dep::call(); + //^ + } + } + """) + } + dir("dep") { + namedMoveToml("Dep") + sources { + move("dep.move", """ + module 0x1::dep { + public fun call() {} + } + """) + } + } + } + assertNotificationAware(event = "initial project creation") + + testProject.checkReferenceIsResolved("sources/main.move", shouldNotResolve = true) + + val moveToml = testProject.file("Move.toml") + runWriteAction { + VfsUtil.saveText(moveToml, VfsUtil.loadText(moveToml).replace("#", "")) + } + assertNotificationAware(moveSystemId, event = "modification in Cargo.toml") + + PsiDocumentManager.getInstance(project).commitAllDocuments() + + scheduleProjectReload() + assertNotificationAware(event = "project reloading") + + runWithInvocationEventsDispatching("Failed to resolve the reference") { + testProject.findElementInFile("sources/main.move").reference?.resolve() != null + } + } + + private fun TestProject.checkFileModification(path: String, triggered: Boolean) { + val file = file(path) + val initialText = VfsUtil.loadText(file) + checkModification("modifying of", path, triggered, + apply = { VfsUtil.saveText(file, "$initialText\nsome text") }, + revert = { VfsUtil.saveText(file, initialText) } + ) + } + + private fun TestProject.checkFileDeletion(path: String, triggered: Boolean) { + val file = file(path) + val initialText = VfsUtil.loadText(file) + checkModification("removing of ", path, triggered, + apply = { file.delete(file.fileSystem) }, + revert = { createFile(rootDirectory, path, initialText) } + ) + } + + private fun TestProject.checkFileCreation(path: String, triggered: Boolean) { + checkModification("creation of", path, triggered, + apply = { createFile(rootDirectory, path) }, + revert = { + val file = file(path) + file.delete(file.fileSystem) + } + ) + } + + private fun checkModification( + eventName: String, + path: String, + triggered: Boolean, + apply: () -> Unit, + revert: () -> Unit + ) { + runWriteAction { + apply() + } + val externalSystems = if (triggered) arrayOf(moveSystemId) else arrayOf() + assertNotificationAware(*externalSystems, event = "$eventName $path") + + runWriteAction { + revert() + } + assertNotificationAware(event = "revert $eventName $path") + } + + private fun scheduleProjectReload() { + val newDisposable = Disposer.newDisposable() + val startLatch = CountDownLatch(1) + val endLatch = CountDownLatch(1) + project.messageBus.connect(newDisposable).subscribe( + MoveProjectsService.MOVE_PROJECTS_REFRESH_TOPIC, + object: MoveProjectsService.MoveProjectsRefreshListener { + override fun onRefreshStarted() { + startLatch.countDown() + } + + override fun onRefreshFinished(status: MoveProjectsService.MoveRefreshStatus) { + endLatch.countDown() + } + } + ) + + try { + projectTracker.scheduleProjectRefresh() + + if (!startLatch.waitFinished(1000)) error("Move project reloading hasn't started") + if (!endLatch.waitFinished(5000)) error("Move project reloading hasn't finished") + } finally { + Disposer.dispose(newDisposable) + } + } + + private fun assertNotificationAware(vararg projects: ExternalSystemProjectId, event: String) { + val message = + if (projects.isEmpty()) "Notification must be expired" else "Notification must be notified" + assertEquals("$message on $event", projects.toSet(), notificationAware.getProjectsWithNotification()) + } + + @Throws(IOException::class) + private fun createFile(root: VirtualFile, path: String, text: String = ""): VirtualFile { + val name = PathUtil.getFileName(path) + val parentPath = PathUtil.getParentPath(path) + var parent = root + if (parentPath.isNotEmpty()) { + parent = VfsUtil.createDirectoryIfMissing(root, parentPath) ?: error("Failed to create $parentPath directory") + } + val file = parent.createChildData(parent.fileSystem, name) + VfsUtil.saveText(file, text) + return file + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/move/cli/toolwindow/MoveProjectsStructureTest.kt b/src/test/kotlin/org/move/cli/toolwindow/MoveProjectsStructureTest.kt index 6cf37eb69..4c10bc6c3 100644 --- a/src/test/kotlin/org/move/cli/toolwindow/MoveProjectsStructureTest.kt +++ b/src/test/kotlin/org/move/cli/toolwindow/MoveProjectsStructureTest.kt @@ -4,11 +4,13 @@ import com.intellij.ide.util.treeView.AbstractTreeStructure import com.intellij.testFramework.PlatformTestUtil import com.intellij.testFramework.ProjectViewTestUtil import org.move.cli.moveProjectsService +import org.move.cli.toolwindow.MoveProjectsTreeStructure.MoveSimpleNode import org.move.utils.tests.MvProjectTestBase import org.move.utils.tests.TreeBuilder -class MoveProjectsStructureTest : MvProjectTestBase() { - fun `test move projects`() = doTest(""" +class MoveProjectsStructureTest: MvProjectTestBase() { + fun `test move projects`() = doTest( + """ Root Project(DepPackage) Dependencies @@ -29,29 +31,57 @@ Root Module(0x1::M) Views View(0x1::M::get_coin_value) - """) { - moveToml(""" + """ + ) { + moveToml( + """ [package] name = "MyPackage" [dependencies] DepPackage = { local = "./dependency" } - """) - sources { main(""" + """ + ) + sources { + main( + """ module 0x1::M { #[view] public fun get_coin_value(): u64 {} entry fun init() { /*caret*/ } } - """) } + """ + ) + } dir("dependency") { namedMoveToml("DepPackage") sources { - move("DepModule.move", """ + move( + "DepModule.move", """ module 0x1::DepModule { entry fun my_init() {} } - """) + """ + ) + } + } + } + + fun `test deeply nested package`() = doTest( + """ +Root + Project(MyPackage) + Dependencies + """ + ) { + dir("root1") { + dir("root2") { + dir("root3") { + namedMoveToml("MyPackage") + sources { + main("/*caret*/") + } + } } } } @@ -67,12 +97,13 @@ Root } /** - * Same as [ProjectViewTestUtil.assertStructureEqual], but uses [CargoSimpleNode.toTestString] instead of [CargoSimpleNode.toString]. + * Same as [ProjectViewTestUtil.assertStructureEqual], + * but uses [MoveSimpleNode.toTestString] instead of [MoveSimpleNode.toString]. */ private fun assertStructureEqual(structure: AbstractTreeStructure, expected: String) { ProjectViewTestUtil.checkGetParentConsistency(structure, structure.rootElement) val actual = PlatformTestUtil.print(structure, structure.rootElement) { - if (it is MoveProjectsTreeStructure.MoveSimpleNode) it.toTestString() else it.toString() + if (it is MoveSimpleNode) it.toTestString() else it.toString() } assertEquals(expected, actual) } diff --git a/src/test/kotlin/org/move/ide/MvBreadcrumbsProviderTest.kt b/src/test/kotlin/org/move/ide/MvBreadcrumbsProviderTest.kt index 76e2bf02a..25911a374 100644 --- a/src/test/kotlin/org/move/ide/MvBreadcrumbsProviderTest.kt +++ b/src/test/kotlin/org/move/ide/MvBreadcrumbsProviderTest.kt @@ -22,7 +22,7 @@ class MvBreadcrumbsProviderTest : MvTestBase() { ) private fun doTextTest(@Language("Move") content: String, info: String) { - inlineFile(content.trimIndent()) + InlineFile(content.trimIndent()) val crumbs = myFixture.breadcrumbsAtCaret.joinToString(separator = "\n") { it.text } UsefulTestCase.assertSameLines(info.trimIndent(), crumbs) } diff --git a/src/test/kotlin/org/move/ide/annotator/HighlightingAnnotatorTest.kt b/src/test/kotlin/org/move/ide/annotator/HighlightingAnnotatorTest.kt index 2fb5a05fe..13d624646 100644 --- a/src/test/kotlin/org/move/ide/annotator/HighlightingAnnotatorTest.kt +++ b/src/test/kotlin/org/move/ide/annotator/HighlightingAnnotatorTest.kt @@ -287,4 +287,14 @@ class HighlightingAnnotatorTest : AnnotatorTestCase(HighlightingAnnotator::class } } """) + + fun `test highlight methods`() = checkHighlighting(""" + module 0x1::m { + struct S { field: u8 } + fun receiver(self: S, self: u8): u8 { self.field } + fun main(s: S) { + s.receiver(); + } + } + """) } diff --git a/src/test/kotlin/org/move/ide/annotator/errors/NeedsTypeAnnotationTest.kt b/src/test/kotlin/org/move/ide/annotator/errors/NeedsTypeAnnotationTest.kt index 52d57f8ed..e391f5c9d 100644 --- a/src/test/kotlin/org/move/ide/annotator/errors/NeedsTypeAnnotationTest.kt +++ b/src/test/kotlin/org/move/ide/annotator/errors/NeedsTypeAnnotationTest.kt @@ -107,7 +107,7 @@ class NeedsTypeAnnotationTest: AnnotatorTestCase(MvErrorAnnotator::class) { } """) - fun `test needs type annotation if missing parameters but not inferrable`() = checkErrors(""" + fun `test needs type annotation if missing params but not those do not affect inference`() = checkErrors(""" module 0x1::m { fun call(a: u8) {} fun main() { @@ -115,4 +115,27 @@ class NeedsTypeAnnotationTest: AnnotatorTestCase(MvErrorAnnotator::class) { } } """) + + fun `test method type arguments inferrable`() = checkErrors(""" + module 0x1::main { + struct S { field: T } + fun receiver(self: &S, param: U): U { + param + } + fun main(s: S) { + let a = s.receiver(1); + } + } + """) + + fun `test method type arguments uninferrable`() = checkErrors(""" + module 0x1::main { + struct S { field: u8 } + fun receiver(self: &S, param: u8): Z { + } + fun main(s: S) { + let a = s.receiver(1); + } + } + """) } diff --git a/src/test/kotlin/org/move/ide/annotator/errors/TypeParametersNumberErrorTest.kt b/src/test/kotlin/org/move/ide/annotator/errors/TypeParametersNumberErrorTest.kt index 2067503bf..6a8451b8e 100644 --- a/src/test/kotlin/org/move/ide/annotator/errors/TypeParametersNumberErrorTest.kt +++ b/src/test/kotlin/org/move/ide/annotator/errors/TypeParametersNumberErrorTest.kt @@ -174,4 +174,16 @@ class TypeParametersNumberErrorTest: AnnotatorTestCase(MvErrorAnnotator::class) } } """) + + fun `test receiver style method missing type parameter`() = checkErrors(""" + module 0x1::main { + struct S { field: T } + fun receiver(self: &S, param: U): U { + param + } + fun main(s: S) { + let b = s.receiver(1); + } + } + """) } diff --git a/src/test/kotlin/org/move/ide/annotator/errors/ValueArgumentsNumberErrorTest.kt b/src/test/kotlin/org/move/ide/annotator/errors/ValueArgumentsNumberErrorTest.kt index d9f844130..bd956f24b 100644 --- a/src/test/kotlin/org/move/ide/annotator/errors/ValueArgumentsNumberErrorTest.kt +++ b/src/test/kotlin/org/move/ide/annotator/errors/ValueArgumentsNumberErrorTest.kt @@ -33,6 +33,22 @@ class ValueArgumentsNumberErrorTest: AnnotatorTestCase(MvErrorAnnotator::class) } """) + fun `test invalid number of parameters receiver style`() = checkErrors(""" + module 0x1::M { + struct S { field: u8 } + fun get_field_0(self: &S): u8 {} + fun get_field_1(self: &S, a: u8): u8 {} + fun get_field_3(self: &S, a: u8, b: u8, c: u8): u8 {} + + fun main(s: S) { + s.get_field_0(4); + s.get_field_1(); + s.get_field_1(1, 4); + s.get_field_3(5, 1); + } + } + """) + fun `test invalid number of parameters with import`() = checkErrors(""" module 0x1::p { public fun params_3(val: u8, val2: u64, s: &signer) {} diff --git a/src/test/kotlin/org/move/ide/hints/InlayParameterHintsTest.kt b/src/test/kotlin/org/move/ide/hints/InlayParameterHintsTest.kt index d9377fc25..9325686cb 100644 --- a/src/test/kotlin/org/move/ide/hints/InlayParameterHintsTest.kt +++ b/src/test/kotlin/org/move/ide/hints/InlayParameterHintsTest.kt @@ -62,7 +62,7 @@ class InlayParameterHintsTest : MvTestBase() { ) private fun checkByText(@Language("Move") code: String) { - inlineFile( + InlineFile( code.trimIndent() .replace(HINT_COMMENT_PATTERN, "<$1/>") ) diff --git a/src/test/kotlin/org/move/ide/hints/InlayTypeHintsTest.kt b/src/test/kotlin/org/move/ide/hints/InlayTypeHintsTest.kt index 1d1be0319..bfde02c34 100644 --- a/src/test/kotlin/org/move/ide/hints/InlayTypeHintsTest.kt +++ b/src/test/kotlin/org/move/ide/hints/InlayTypeHintsTest.kt @@ -65,7 +65,7 @@ class InlayTypeHintsTest : MvTestBase() { """) private fun checkByText(@Language("Move") code: String) { - inlineFile( + InlineFile( code.trimIndent() .replace(HINT_COMMENT_PATTERN, "<$1/>") ) diff --git a/src/test/kotlin/org/move/ide/inspections/FieldInitShorthandInspectionTest.kt b/src/test/kotlin/org/move/ide/inspections/FieldInitShorthandInspectionTest.kt index 412cd1f24..f6439f517 100644 --- a/src/test/kotlin/org/move/ide/inspections/FieldInitShorthandInspectionTest.kt +++ b/src/test/kotlin/org/move/ide/inspections/FieldInitShorthandInspectionTest.kt @@ -1,20 +1,21 @@ package org.move.ide.inspections +import org.intellij.lang.annotations.Language import org.move.utils.tests.annotation.InspectionTestBase -class FieldInitShorthandInspectionTest : InspectionTestBase(FieldInitShorthandInspection::class) { +class FieldInitShorthandInspectionTest: InspectionTestBase(FieldInitShorthandInspection::class) { - fun `test not applicable`() = checkFixIsUnavailable( + fun `test not applicable`() = doFixIsUnavailableTest( "Use initialization shorthand", """ module 0x1::M { fun m() { let _ = S { foo: bar/*caret*/, baz: &baz }; } } - """, checkWeakWarn = true + """ ) - fun `test fix for struct literal`() = checkFixByText( + fun `test fix for struct literal`() = doFixTest( "Use initialization shorthand", """ module 0x1::M { fun m() { @@ -30,7 +31,7 @@ class FieldInitShorthandInspectionTest : InspectionTestBase(FieldInitShorthandIn """ ) - fun `test fix for struct pattern`() = checkFixByText( + fun `test fix for struct pattern`() = doFixTest( "Use pattern shorthand", """ module 0x1::M { struct S { foo: u8 } @@ -50,8 +51,9 @@ class FieldInitShorthandInspectionTest : InspectionTestBase(FieldInitShorthandIn """ ) - fun `test fix for schema literal`() = checkFixByText( - "Use initialization shorthand", """ + fun `test fix for schema literal`() = doFixTest( + "Use initialization shorthand", + """ module 0x1::M { spec module { include Schema { foo: foo/*caret*/ }; @@ -65,4 +67,30 @@ class FieldInitShorthandInspectionTest : InspectionTestBase(FieldInitShorthandIn } """ ) + + private fun doTest( + @Language("Move") text: String, + ) = + checkByText(text, checkWarn = false, checkWeakWarn = true) + + private fun doFixTest( + fixName: String, + @Language("Move") before: String, + @Language("Move") after: String, + + ) = + checkFixByText( + fixName, before, after, + checkWarn = false, checkWeakWarn = true + ) + + private fun doFixIsUnavailableTest( + fixName: String, + @Language("Move") text: String, + ) = + checkFixIsUnavailable( + fixName, + text, + checkWarn = false, checkWeakWarn = true + ) } diff --git a/src/test/kotlin/org/move/ide/inspections/MvMissingAcquiresInspectionTest.kt b/src/test/kotlin/org/move/ide/inspections/MvMissingAcquiresInspectionTest.kt index 2e04ddab3..2fbb000ed 100644 --- a/src/test/kotlin/org/move/ide/inspections/MvMissingAcquiresInspectionTest.kt +++ b/src/test/kotlin/org/move/ide/inspections/MvMissingAcquiresInspectionTest.kt @@ -183,7 +183,7 @@ module 0x1::main { """ ) - fun `test outer function requires acquires through inline function`() = checkWarnings(""" + fun `test outer function requires acquires through inline function`() = checkErrors(""" module 0x1::main { struct S has key {} fun call() { @@ -194,4 +194,17 @@ module 0x1::main { } } """) + + fun `test missing acquires with receiver style`() = checkErrors( + """ + module 0x1::M { + struct S has key {} + fun acquire(self: &S) acquires S { + borrow_global(@0x1); + } + fun main(s: S) { + s.acquire(); + } + } + """) } diff --git a/src/test/kotlin/org/move/ide/inspections/MvUnusedAcquiresTypeInspectionTest.kt b/src/test/kotlin/org/move/ide/inspections/MvUnusedAcquiresTypeInspectionTest.kt index 9e7e7583b..adaa0eb33 100644 --- a/src/test/kotlin/org/move/ide/inspections/MvUnusedAcquiresTypeInspectionTest.kt +++ b/src/test/kotlin/org/move/ide/inspections/MvUnusedAcquiresTypeInspectionTest.kt @@ -2,7 +2,7 @@ package org.move.ide.inspections import org.move.utils.tests.annotation.InspectionTestBase -class MvUnusedAcquiresTypeInspectionTest : InspectionTestBase(MvUnusedAcquiresTypeInspection::class) { +class MvUnusedAcquiresTypeInspectionTest: InspectionTestBase(MvUnusedAcquiresTypeInspection::class) { fun `test no error if used acquires type`() = checkWarnings( """ module 0x1::M { @@ -14,41 +14,52 @@ class MvUnusedAcquiresTypeInspectionTest : InspectionTestBase(MvUnusedAcquiresTy """ ) - fun `test error if unused acquires type`() = checkFixByText("Remove acquires", + fun `test error if unused acquires type`() = checkWarnings( """ module 0x1::M { struct S has key {} fun call() /*caret*/acquires S { } } - """, """ + """) + + fun `test no error if acquires type with receiver style`() = checkWarnings( + """ module 0x1::M { struct S has key {} - fun call() { + fun acquire(self: &S) acquires S { + borrow_global(@0x1); + } + fun main(s: S) acquires S { + s.acquire(); } } - """ - ) + """) - fun `test error if duplicate acquires type`() = checkFixByText("Remove acquires", + fun `test error if redundant acquires with receiver style`() = checkWarnings( """ module 0x1::M { struct S has key {} - fun call() acquires S, /*caret*/S { - borrow_global(@0x1); + fun acquire(self: &S) { + } + fun main(s: S) acquires S { + s.acquire(); } } - """, """ + """) + + fun `test error if duplicate acquires type`() = checkWarnings( + """ module 0x1::M { struct S has key {} - fun call() acquires S { + fun call() acquires S, /*caret*/S { borrow_global(@0x1); } } """ ) - fun `test warn if type not declared in the current module`() = checkFixByText("Remove acquires", + fun `test warn if type not declared in the current module`() = checkWarnings( """ module 0x1::M { struct S has key {} @@ -62,23 +73,11 @@ class MvUnusedAcquiresTypeInspectionTest : InspectionTestBase(MvUnusedAcquiresTy M::call(); } } - """, """ - module 0x1::M { - struct S has key {} - public fun call() acquires S { - borrow_global(@0x1); - } - } - module 0x1::M2 { - use 0x1::M::{Self, S}; - fun call() { - M::call(); - } - } """ ) - fun `test no unused acquires for borrow_global with dot expr`() = checkWarnings(""" + fun `test no unused acquires for borrow_global with dot expr`() = checkWarnings( + """ module 0x1::main { struct StakePool has key { locked_until_secs: u64, @@ -87,9 +86,11 @@ module 0x1::main { borrow_global(pool_address).locked_until_secs } } - """) + """ + ) - fun `test no unused acquires with inline function`() = checkWarnings(""" + fun `test no unused acquires with inline function`() = checkWarnings( + """ module 0x1::main { struct StakePool has key { locked_until_secs: u64, @@ -101,9 +102,11 @@ module 0x1::main { borrow_global(pool_address); } } - """) + """ + ) - fun `test no unused acquires if declared on inline function`() = checkWarnings(""" + fun `test no unused acquires if declared on inline function`() = checkWarnings( + """ module 0x1::main { struct StakePool has key { locked_until_secs: u64, @@ -115,9 +118,11 @@ module 0x1::main { borrow_global(pool_address); } } - """) + """ + ) - fun `test error if declared on inline function but not acquired`() = checkWarnings(""" + fun `test error if declared on inline function but not acquired`() = checkWarnings( + """ module 0x1::main { struct StakePool has key { locked_until_secs: u64, @@ -125,9 +130,11 @@ module 0x1::main { inline fun get_lockup_secs(pool_address: address) acquires StakePool { } } - """) + """ + ) - fun `test no unused acquires if declared on inline function but not acquired nested`() = checkWarnings(""" + fun `test no unused acquires if declared on inline function but not acquired nested`() = checkWarnings( + """ module 0x1::main { struct StakePool has key { locked_until_secs: u64, @@ -138,5 +145,6 @@ module 0x1::main { inline fun f() { } } - """) + """ + ) } diff --git a/src/test/kotlin/org/move/ide/inspections/ReplaceWithMethodCallInspectionProjectTest.kt b/src/test/kotlin/org/move/ide/inspections/ReplaceWithMethodCallInspectionProjectTest.kt new file mode 100644 index 000000000..9823886ca --- /dev/null +++ b/src/test/kotlin/org/move/ide/inspections/ReplaceWithMethodCallInspectionProjectTest.kt @@ -0,0 +1,122 @@ +package org.move.ide.inspections + +import org.intellij.lang.annotations.Language +import org.move.utils.tests.FileTreeBuilder +import org.move.utils.tests.annotation.InspectionProjectTestBase + +class ReplaceWithMethodCallInspectionProjectTest: + InspectionProjectTestBase(ReplaceWithMethodCallInspection::class) { + + fun `test no warning with vector as self param with local method`() = doTest { + namedMoveToml("MyPackage") + sources { + main( + """ + module 0x1::main { + native fun length(self: &vector): u8; + fun main() { + /*caret*/length(vector[1, 2, 3]); + } + } + """ + ) + } + } + + fun `test no warning with vector as self param with module with wrong address`() = doTest { + namedMoveToml("MyPackage") + sources { + move( + "vector.move", """ + module 0x2::vector { + public native fun length(self: &vector): u8; + } + """ + ) + main( + """ + module 0x1::main { + use 0x2::vector::length; + fun main() { + /*caret*/length(vector[1, 2, 3]); + } + } + """ + ) + } + } + + fun `test fix if vector method from correct address module`() = doFixTest( + { + namedMoveToml("MyPackage") + sources { + move( + "vector.move", """ + module 0x1::vector { + public native fun length(self: &vector): u8; + } + """ + ) + main( + """ + module 0x1::main { + use 0x1::vector::length; + fun main() { + /*caret*/length(vector[1, 2, 3]); + } + } + """ + ) + } + }, """ + module 0x1::main { + use 0x1::vector::length; + fun main() { + vector[1, 2, 3].length(); + } + } + """) + + fun `test fix if vector reference method from correct address module`() = doFixTest( + { + namedMoveToml("MyPackage") + sources { + move( + "vector.move", """ + module 0x1::vector { + public native fun length(self: &vector): u8; + } + """ + ) + main( + """ + module 0x1::main { + use 0x1::vector::length; + fun main() { + /*caret*/length(&vector[1, 2, 3]); + } + } + """ + ) + } + }, """ + module 0x1::main { + use 0x1::vector::length; + fun main() { + vector[1, 2, 3].length(); + } + } + """) + + private fun doTest(code: FileTreeBuilder.() -> Unit) = + checkByFileTree(code, checkWarn = false, checkWeakWarn = true) + + private fun doFixTest( + before: FileTreeBuilder.() -> Unit, + @Language("Move") after: String, + ) = + checkFixByFileTree( + "Replace with method call", before, after, + checkWarn = false, checkWeakWarn = true + ) +} \ No newline at end of file diff --git a/src/test/kotlin/org/move/ide/inspections/ReplaceWithMethodCallInspectionTest.kt b/src/test/kotlin/org/move/ide/inspections/ReplaceWithMethodCallInspectionTest.kt new file mode 100644 index 000000000..38455ed23 --- /dev/null +++ b/src/test/kotlin/org/move/ide/inspections/ReplaceWithMethodCallInspectionTest.kt @@ -0,0 +1,284 @@ +package org.move.ide.inspections + +import org.intellij.lang.annotations.Language +import org.move.utils.tests.annotation.InspectionTestBase + +class ReplaceWithMethodCallInspectionTest: InspectionTestBase(ReplaceWithMethodCallInspection::class) { + + fun `test no warning if first parameter is not self`() = doTest( + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(s: &S): u8 { s.field } + fun main(s: S) { + get_field(&s); + } + } + """ + ) + + fun `test no warning if first parameter has different type`() = doTest( + """ + module 0x1::main { + struct S { field: u8 } + struct T { field: u8 } + fun get_field(self: &T): u8 { s.field } + fun main(s: S) { + get_field(&s); + } + } + """ + ) + + fun `test no warning if references are incompatible`() = doTest( + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(s: &mut S): u8 { s.field } + fun main(s: &S) { + get_field(s); + } + } + """ + ) + + fun `test no warning if self parameter struct is from another module`() = doTest( + """ + module 0x1::m { + struct S { field: u8 } + } + module 0x1::main { + use 0x1::m::S; + fun get_field(self: S): u8 { s.field } + fun main(s: S) { + get_field(s); + } + } + """ + ) + + fun `test no warning if self parameter is not provided`() = doTest( + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(s: S): u8 { s.field } + fun main(s: S) { + get_field(); + } + } + """ + ) + + fun `test no warning if not enough parameters`() = doTest( + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(s: S, a: u8, b: u8): u8 { s.field } + fun main(s: S) { + get_field(s, 1); + } + } + """ + ) + + fun `test no warning if generics are incompatible`() = doTest( + """ + module 0x1::main { + struct S { field: T } + fun get_field(self: &S): u8 { s.field } + fun main(s: &S) { + get_field(s); + } + } + """ + ) + + fun `test no warning if generic is unknown`() = doTest( + """ + module 0x1::main { + struct S { field: T } + fun get_field(self: &S): u8 { s.field } + fun main(s: &S) { + get_field(s); + } + } + """ + ) + + fun `test fix if method`() = doFixTest( + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(self: S): u8 { s.field } + fun main(s: S) { + /*caret*/get_field(s); + } + } + """, + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(self: S): u8 { s.field } + fun main(s: S) { + s.get_field(); + } + } + """, + ) + + fun `test fix if method with parameters`() = doFixTest( + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(self: S, a: u8, b: u8): u8 { s.field } + fun main(s: S) { + /*caret*/get_field(s, 3, 4); + } + } + """, + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(self: S, a: u8, b: u8): u8 { s.field } + fun main(s: S) { + s.get_field(3, 4); + } + } + """, + ) + + fun `test fix if method of imported struct`() = doFixTest( + """ + module 0x1::m { + struct S { field: u8 } + public fun get_field(self: S): u8 { s.field } + } + module 0x1::main { + use 0x1::m::S; + use 0x1::m::get_field; + fun main(s: S) { + /*caret*/get_field(s); + } + } + """, + """ + module 0x1::m { + struct S { field: u8 } + public fun get_field(self: S): u8 { s.field } + } + module 0x1::main { + use 0x1::m::S; + use 0x1::m::get_field; + fun main(s: S) { + s.get_field(); + } + } + """, + ) + + fun `test fix if method and autoborrow`() = doFixTest( + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(self: &S): u8 { s.field } + fun main(s: S) { + /*caret*/get_field(&s); + } + } + """, + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(self: &S): u8 { s.field } + fun main(s: S) { + s.get_field(); + } + } + """, + ) + + fun `test fix if method and compatible reference`() = doFixTest( + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(self: &S): u8 { s.field } + fun main(s: &mut S) { + /*caret*/get_field(s); + } + } + """, + """ + module 0x1::main { + struct S { field: u8 } + fun get_field(self: &S): u8 { s.field } + fun main(s: &mut S) { + s.get_field(); + } + } + """, + ) + + fun `test replace with method call tranfer type arguments`() = doFixTest(""" + module 0x1::main { + struct S { field: u8 } + native fun get_type(self: &S): T; + fun main(s: S) { + /*caret*/get_type(&s); + } + } + """, """ + module 0x1::main { + struct S { field: u8 } + native fun get_type(self: &S): T; + fun main(s: S) { + s.get_type(); + } + } + """) + + fun `test replace with deref expr`() = doFixTest(""" + module 0x1::main { + struct String { bytes: vector } + public native fun sub_string(self: &String, i: u64, j: u64): String; + fun main(key: &String) { + /*caret*/sub_string(&*key, 1, 2); + } + } + """, """ + module 0x1::main { + struct String { bytes: vector } + public native fun sub_string(self: &String, i: u64, j: u64): String; + fun main(key: &String) { + (*key).sub_string(1, 2); + } + } + """) + + fun `test replace with copy expr`() = doFixTest(""" + module 0x1::main { + struct String { bytes: vector } + public native fun sub_string(self: &String, i: u64, j: u64): String; + fun main(key: &String) { + /*caret*/sub_string(copy key, 1, 2); + } + } + """, """ + module 0x1::main { + struct String { bytes: vector } + public native fun sub_string(self: &String, i: u64, j: u64): String; + fun main(key: &String) { + (copy key).sub_string(1, 2); + } + } + """) + + private fun doTest(@Language("Move") text: String) = + checkByText(text, checkWarn = false, checkWeakWarn = true) + + private fun doFixTest( + @Language("Move") before: String, + @Language("Move") after: String, + ) = + checkFixByText("Replace with method call", before, after, + checkWarn = false, checkWeakWarn = true) +} \ No newline at end of file diff --git a/src/test/kotlin/org/move/ide/refactoring/RenameTest.kt b/src/test/kotlin/org/move/ide/refactoring/RenameTest.kt index 275f9a9b4..c93d68857 100644 --- a/src/test/kotlin/org/move/ide/refactoring/RenameTest.kt +++ b/src/test/kotlin/org/move/ide/refactoring/RenameTest.kt @@ -487,7 +487,7 @@ class RenameTest : MvTestBase() { val fileText = """ module 0x1::Main { use 0x1::Main; } """ - inlineFile(fileText, "Main.move") + InlineFile(fileText, "Main.move") myFixture.renameElement(myFixture.file, "MyMain.move") @@ -556,12 +556,31 @@ class RenameTest : MvTestBase() { } """) + fun `test rename parameter field init shorthand replaced`() = doTest("self", """ + module 0x1::main { + struct Option { vec: vector }; + public fun from_vec(/*caret*/vec: vector): Option { + assert!(vec.length() <= 1, EOPTION_VEC_TOO_LONG); + Option { vec } + } + } + """, """ + module 0x1::main { + struct Option { vec: vector }; + public fun from_vec(self: vector): Option { + assert!(self.length() <= 1, EOPTION_VEC_TOO_LONG); + Option { vec: self } + } + } + """ + ) + private fun doTest( newName: String, @Language("Move") before: String, @Language("Move") after: String, ) { - inlineFile(before).withCaret() + InlineFile(before).withCaret() val element = myFixture.elementAtCaret myFixture.renameElement(element, newName, false, false) myFixture.checkResult(after) @@ -574,7 +593,7 @@ class RenameTest : MvTestBase() { @Language("Move") before: String, @Language("Move") after: String, ) { - inlineFile(before, name = beforeFileName).withCaret() + InlineFile(before, name = beforeFileName).withCaret() val element = myFixture.elementAtCaret myFixture.renameElement(element, newName, false, false) myFixture.checkResult(after) diff --git a/src/test/kotlin/org/move/ide/search/FindUsagesTest.kt b/src/test/kotlin/org/move/ide/search/FindUsagesTest.kt index 3404b6c6d..29e86ee93 100644 --- a/src/test/kotlin/org/move/ide/search/FindUsagesTest.kt +++ b/src/test/kotlin/org/move/ide/search/FindUsagesTest.kt @@ -42,7 +42,7 @@ class FindUsagesTest : MvTestBase() { """) private fun doTestByText(@Language("Move") code: String) { - inlineFile(code) + InlineFile(code) val (_, _, offset) = myFixture.findElementWithDataAndOffsetInEditor() val source = TargetElementUtil.getInstance().findTargetElement( diff --git a/src/test/kotlin/org/move/lang/completion/CompletionPrioritiesTest.kt b/src/test/kotlin/org/move/lang/completion/CompletionPrioritiesTest.kt index fb224077f..6382671ec 100644 --- a/src/test/kotlin/org/move/lang/completion/CompletionPrioritiesTest.kt +++ b/src/test/kotlin/org/move/lang/completion/CompletionPrioritiesTest.kt @@ -125,22 +125,88 @@ module 0x1::Main { """ ) - fun checkCompletionsOrder(listStart: List, @Language("Move") code: String) { + fun `test field type`() = checkCompletionsOrder( + listOf("field2", "field1"), + """ + module 0x1::main { + struct S { field1: u8, field2: u16 } + fun get_member1(self: &S): u8 { s.field1 } + fun get_member2(self: &S): u16 { s.field2 } + fun main(s: S) { + let a: u16 = s.fi/*caret*/ + } + } + """ + ) + + fun `test method return type`() = checkCompletionsOrder( + listOf("get_member2", "get_member1"), + """ + module 0x1::main { + struct S { field1: u8, field2: u16 } + fun get_member1(self: &S): u8 { s.field1 } + fun get_member2(self: &S): u16 { s.field2 } + fun main(s: S) { + let a: u16 = s.get_m/*caret*/ + } + } + """ + ) + + fun `test method return type with ref`() = checkCompletionsOrder( + listOf("borrow", "borrow_with_default", "borrow_buckets"), + """ + module 0x1::main { + struct S { field: T } + fun borrow(self: &S): &T {} + fun borrow_buckets(self: &S): &vector {} + fun borrow_with_default(self: &S): &T {} + fun main(s: S): &T { + s.b/*caret*/ + } + } + """ + ) + + fun `test field type with ref`() = checkCompletionsOrder( + listOf("borrow", "borrow_with_default", "borrow_buckets"), + """ + module 0x1::main { + struct S { + borrow: T, + borrow_buckets: vector, + borrow_with_default: T, + } + fun main(s: S): T { + s.b/*caret*/ + } + } + """ + ) + + private fun checkCompletionsOrder(listStart: List, @Language("Move") code: String) { val variants = completionFixture.invokeCompletion(code) val lookupStrings = variants.map { it.lookupString } - checkValidPrefix(listStart, lookupStrings) + checkCompletionListStartsWith(listStart, lookupStrings) } - fun checkFqCompletionsOrder(listStart: List, @Language("Move") code: String) { + private fun checkFqCompletionsOrder(listStart: List, @Language("Move") code: String) { val variants = completionFixture.invokeCompletion(code) val lookupStrings = variants.map { (it.psiElement as? MvQualNamedElement)?.qualName?.editorText() ?: it.lookupString } - checkValidPrefix(listStart, lookupStrings) + checkCompletionListStartsWith(listStart, lookupStrings) } - private fun checkValidPrefix(prefix: List, lookups: List) { - check(lookups.subList(0, prefix.size) == prefix) { - "Expected variants \n $prefix\n ain't a prefix of actual \n $lookups" + private fun checkCompletionListStartsWith(prefix: List, completions: List) { + check(completions.size >= prefix.size) { + "Completions list is smaller than expected prefix. \n" + + " expected prefix: $prefix \n" + + " actual completions: $completions" + } + check(completions.subList(0, prefix.size) == prefix) { + "Wrong order of completions. \n" + + " expected prefix: $prefix \n" + + " actual completions: $completions" } } } diff --git a/src/test/kotlin/org/move/lang/completion/lookups/BuiltInFunctionLookupTest.kt b/src/test/kotlin/org/move/lang/completion/lookups/BuiltInFunctionLookupTest.kt index f8cc91c58..b13fc4ab1 100644 --- a/src/test/kotlin/org/move/lang/completion/lookups/BuiltInFunctionLookupTest.kt +++ b/src/test/kotlin/org/move/lang/completion/lookups/BuiltInFunctionLookupTest.kt @@ -2,13 +2,15 @@ package org.move.lang.completion.lookups import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementPresentation -import org.move.lang.core.completion.createCompletionLookupElement +import org.move.lang.core.completion.CompletionContext +import org.move.lang.core.completion.createLookupElement import org.move.lang.core.psi.MvModule import org.move.lang.core.psi.ext.builtinFunctions +import org.move.lang.core.resolve.ContextScopeInfo import org.move.utils.tests.MvTestBase import org.move.utils.tests.base.findElementInEditor -class BuiltInFunctionLookupTest : MvTestBase() { +class BuiltInFunctionLookupTest: MvTestBase() { fun `test move_from`() = checkBuiltinPresentation( "move_from", tailText = "(addr: address): T", @@ -38,10 +40,12 @@ class BuiltInFunctionLookupTest : MvTestBase() { module 0x1::M {} //^ """ - inlineFile(moduleText) + InlineFile(moduleText) val moduleElement = myFixture.findElementInEditor() val lookup = - moduleElement.builtinFunctions().single { it.name == name }.createCompletionLookupElement() + moduleElement.builtinFunctions().single { it.name == name }.let { + it.createLookupElement(CompletionContext(it, ContextScopeInfo.default())) + } checkLookupPresentation( lookup, tailText = tailText, diff --git a/src/test/kotlin/org/move/lang/completion/lookups/LookupElementTest.kt b/src/test/kotlin/org/move/lang/completion/lookups/LookupElementTest.kt index 4a562c527..a869039fa 100644 --- a/src/test/kotlin/org/move/lang/completion/lookups/LookupElementTest.kt +++ b/src/test/kotlin/org/move/lang/completion/lookups/LookupElementTest.kt @@ -1,64 +1,142 @@ package org.move.lang.completion.lookups +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.completion.CompletionSorter +import com.intellij.codeInsight.completion.PrefixMatcher import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementPresentation +import com.intellij.patterns.ElementPattern import com.intellij.psi.NavigatablePsiElement import org.intellij.lang.annotations.Language -import org.move.lang.core.completion.createCompletionLookupElement +import org.move.lang.core.completion.CompletionContext +import org.move.lang.core.completion.createLookupElement +import org.move.lang.core.completion.providers.MethodOrFieldCompletionProvider import org.move.lang.core.psi.MvElement import org.move.lang.core.psi.MvNamedElement +import org.move.lang.core.psi.ext.MvMethodOrField +import org.move.lang.core.resolve.ContextScopeInfo +import org.move.lang.core.resolve.ref.MvReferenceElement import org.move.utils.tests.MvTestBase import org.move.utils.tests.base.findElementInEditor -class LookupElementTest : MvTestBase() { - fun `test function param`() = check(""" +class LookupElementTest: MvTestBase() { + fun `test function param`() = check( + """ module 0x1::M { fun call(a: u8) { //^ } } - """, typeText = "u8") + """, typeText = "u8" + ) - fun `test function`() = check(""" + fun `test function`() = check( + """ module 0x1::M { fun call(x: u64, account: &signer): u8 {} //^ } - """, tailText = "(x: u64, account: &signer): u8", typeText = "main.move") + """, tailText = "(x: u64, account: &signer): u8", typeText = "main.move" + ) - fun `test multiline params function`() = check(""" + fun `test multiline params function`() = check( + """ module 0x1::M { fun call(x: u64, account: &signer): u8 {} //^ } - """, tailText = "(x: u64, account: &signer): u8", typeText = "main.move") + """, tailText = "(x: u64, account: &signer): u8", typeText = "main.move" + ) - fun `test const item`() = check(""" + fun `test const item`() = check( + """ module 0x1::M { const MY_CONST: u8 = 1; //^ } - """, typeText = "u8") + """, typeText = "u8" + ) - fun `test struct`() = check(""" + fun `test struct`() = check( + """ module 0x1::M { struct MyStruct { val: u8 } //^ } - """, tailText = " { ... }", typeText = "main.move") + """, tailText = " { ... }", typeText = "main.move" + ) - fun `test module`() = check(""" + fun `test module`() = check( + """ address 0x1 { module M {} //^ } - """, tailText = " 0x1", typeText = "main.move") + """, tailText = " 0x1", typeText = "main.move" + ) - fun `test module with named address`() = check(""" + fun `test module with named address`() = check( + """ module Std::M {} //^ - """, tailText = " Std", typeText = "main.move") + """, tailText = " Std", typeText = "main.move" + ) + + fun `test generic field`() = checkMethodOrFieldProvider( + """ + module 0x1::main { + struct S { field: T } + fun main() { + let s = S { field: 1u8 }; + s.field; + //^ + } + } + """, typeText = "u8" + ) + + fun `test generic method`() = checkMethodOrFieldProvider( + """ + module 0x1::main { + struct S { field: T } + fun receiver(self: S): T {} + fun main() { + let s = S { field: 1u8 }; + s.receiver(); + //^ + } + } + """, tailText = "(self)", typeText = "u8" + ) + + fun `test generic method ref`() = checkMethodOrFieldProvider( + """ + module 0x1::main { + struct S { field: T } + fun receiver(self: &S): T {} + fun main() { + let s = S { field: 1u8 }; + s.receiver(); + //^ + } + } + """, tailText = "(&self)", typeText = "u8" + ) + + fun `test generic method ref mut`() = checkMethodOrFieldProvider( + """ + module 0x1::main { + struct S { field: T } + fun receiver(self: &mut S): T {} + fun main() { + let s = S { field: 1u8 }; + s.receiver(); + //^ + } + } + """, tailText = "(&mut self)", typeText = "u8" + ) private fun check( @Language("Move") code: String, @@ -74,13 +152,55 @@ class LookupElementTest : MvTestBase() { typeText: String? = null, isBold: Boolean = false, isStrikeout: Boolean = false, - ) where T : NavigatablePsiElement, T : MvElement { - inlineFile(code) + ) where T: NavigatablePsiElement, T: MvElement { + InlineFile(code) val element = myFixture.findElementInEditor() as? MvNamedElement ?: error("Marker `^` should point to the MvNamedElement") - val lookup = element.createCompletionLookupElement() + val completionCtx = CompletionContext(element, ContextScopeInfo.default()) + val lookup = element.createLookupElement(completionCtx) + checkLookupPresentation( + lookup, + tailText = tailText, + typeText = typeText, + isBold = isBold, + isStrikeout = isStrikeout + ) + } + + private fun checkMethodOrFieldProvider( + @Language("Move") code: String, + tailText: String? = null, + typeText: String? = null, + isBold: Boolean = false, + isStrikeout: Boolean = false + ) { + InlineFile(code) + val element = findElementInEditor() + + val lookups = mutableListOf() + val result = object: CompletionResultSet(PrefixMatcher.ALWAYS_TRUE, null, null) { + override fun caseInsensitive(): CompletionResultSet = this + override fun withPrefixMatcher(matcher: PrefixMatcher): CompletionResultSet = this + override fun withPrefixMatcher(prefix: String): CompletionResultSet = this + override fun restartCompletionOnPrefixChange(prefixCondition: ElementPattern?) {} + override fun addLookupAdvertisement(text: String) {} + override fun withRelevanceSorter(sorter: CompletionSorter): CompletionResultSet = this + override fun restartCompletionWhenNothingMatches() {} + override fun addElement(element: LookupElement) { + lookups.add(element) + } + } + + if (element is MvMethodOrField) { + MethodOrFieldCompletionProvider.addMethodOrFieldVariants(element, result) + } + + val lookup = lookups.single { + val namedElement = it.psiElement as? MvNamedElement + namedElement?.name == element.referenceName + } checkLookupPresentation( lookup, tailText = tailText, diff --git a/src/test/kotlin/org/move/lang/completion/names/DotAccessCompletionTest.kt b/src/test/kotlin/org/move/lang/completion/names/DotAccessCompletionTest.kt index 96602522c..b7794b404 100644 --- a/src/test/kotlin/org/move/lang/completion/names/DotAccessCompletionTest.kt +++ b/src/test/kotlin/org/move/lang/completion/names/DotAccessCompletionTest.kt @@ -73,4 +73,94 @@ module 0x1::M { } } """) + + fun `test receiver style function completion`() = doSingleCompletion(""" + module 0x1::main { + struct S { field: u8 } + fun receiver(self: &S): u8 {} + fun main(s: S) { + s.rece/*caret*/ + } + } + """, """ + module 0x1::main { + struct S { field: u8 } + fun receiver(self: &S): u8 {} + fun main(s: S) { + s.receiver()/*caret*/ + } + } + """) + + fun `test receiver style function completion with assignment`() = doSingleCompletion(""" + module 0x1::main { + struct S { field: u8 } + fun receiver(self: &S): Z {} + fun main(s: S) { + let f: u8 = s.rece/*caret*/ + } + } + """, """ + module 0x1::main { + struct S { field: u8 } + fun receiver(self: &S): Z {} + fun main(s: S) { + let f: u8 = s.receiver()/*caret*/ + } + } + """) + + fun `test receiver style function completion from another module`() = doSingleCompletion(""" + module 0x1::m { + struct S { field: u8 } + public fun receiver(self: &S): u8 {} + } + module 0x1::main { + use 0x1::m::S; + fun main(s: S) { + s.rece/*caret*/ + } + } + """, """ + module 0x1::m { + struct S { field: u8 } + public fun receiver(self: &S): u8 {} + } + module 0x1::main { + use 0x1::m::S; + fun main(s: S) { + s.receiver()/*caret*/ + } + } + """) + + fun `test receiver style function completion type annotation required`() = doSingleCompletion(""" + module 0x1::main { + struct S { field: u8 } + fun receiver(self: &S): Z {} + fun main(s: S) { + s.rece/*caret*/; + } + } + """, """ + module 0x1::main { + struct S { field: u8 } + fun receiver(self: &S): Z {} + fun main(s: S) { + s.receiver(); + } + } + """) + + fun `test fields are not available from another module`() = checkNoCompletion(""" + module 0x1::m { + struct S { field: u8 } + } + module 0x1::main { + use 0x1::m::S; + fun main(s: S) { + s.fi/*caret*/ + } + } + """) } diff --git a/src/test/kotlin/org/move/lang/resolve/ResolveFunctionTest.kt b/src/test/kotlin/org/move/lang/resolve/ResolveFunctionTest.kt index c45580f16..0f93076c0 100644 --- a/src/test/kotlin/org/move/lang/resolve/ResolveFunctionTest.kt +++ b/src/test/kotlin/org/move/lang/resolve/ResolveFunctionTest.kt @@ -725,4 +725,176 @@ module 0x1::mod { } } """) + + fun `test resolve receiver function`() = checkByCode(""" + module 0x1::main { + struct S { x: u64 } + fun receiver(self: S, y: u64): u64 { + //X + self.x + y + } + fun test_call_styles(s: S): u64 { + s.receiver(1) + //^ + } + } + """) + + fun `test resolve receiver function with reference`() = checkByCode(""" + module 0x1::main { + struct S { x: u64 } + fun receiver_ref(self: &S, y: u64): u64 { + //X + self.x + y + } + fun test_call_styles(s: S): u64 { + s.receiver_ref(1) + //^ + } + } + """) + + fun `test resolve receiver function with mut reference`() = checkByCode(""" + module 0x1::main { + struct S { x: u64 } + fun receiver_mut_ref(self: &mut S, y: u64): u64 { + //X + self.x + y + } + fun test_call_styles(s: S): u64 { + s.receiver_mut_ref(1) + //^ + } + } + """) + + fun `test resolve receiver function with inline mut reference`() = checkByCode(""" + module 0x1::main { + struct S { x: u64 } + inline fun receiver_mut_ref(self: &mut S, y: u64): u64 { + //X + self.x + y + } + fun test_call_styles(s: S): u64 { + s.receiver_mut_ref(1) + //^ + } + } + """) + + fun `test cannot be resolved if self has another type`() = checkByCode(""" + module 0x1::main { + struct S { x: u64 } + struct T { x: u64 } + fun receiver(self: T, y: u64): u64 { + self.x + y + } + fun test_call_styles(s: S): u64 { + s.receiver(1) + //^ unresolved + } + } + """) + + fun `test cannot be resolved if self requires mutable reference`() = checkByCode(""" + module 0x1::main { + struct S { x: u64 } + fun receiver(self: &mut S, y: u64): u64 { + self.x + y + } + fun test_call_styles(s: &S): u64 { + s.receiver(1) + //^ unresolved + } + } + """) + + fun `test cannot be resolved if self requires no reference`() = checkByCode(""" + module 0x1::main { + struct S { x: u64 } + fun receiver(self: S, y: u64): u64 { + self.x + y + } + fun test_call_styles(s: &mut S): u64 { + s.receiver(1) + //^ unresolved + } + } + """) + + fun `test receiver resolvable if self requires reference but mut reference exists`() = checkByCode(""" + module 0x1::main { + struct S { x: u64 } + fun receiver(self: &S, y: u64): u64 { + //X + self.x + y + } + fun test_call_styles(s: &mut S): u64 { + s.receiver(1) + //^ + } + } + """) + + fun `test public receiver style from another module`() = checkByCode(""" + module 0x1::m { + struct S { x: u64 } + public fun receiver(self: S, y: u64): u64 { + //X + self.x + y + } + } + module 0x1::main { + use 0x1::m::S; + + fun test_call_styles(s: S): u64 { + s.receiver(1) + //^ + } + } + """) + + fun `test unresolved private receiver style from another module`() = checkByCode(""" + module 0x1::m { + struct S { x: u64 } + fun receiver(self: S, y: u64): u64 { + self.x + y + } + } + module 0x1::main { + use 0x1::m::S; + + fun test_call_styles(s: S): u64 { + s.receiver(1) + //^ unresolved + } + } + """) + + fun `test resolve receiver style with generic argument`() = checkByCode(""" + module 0x1::main { + struct S { field: T } + fun receiver(self: S): T { + //X + self.field + } + fun main(s: S) { + s.receiver() + //^ + } + } + """) + + fun `test method cannot be resolved if self is not the first parameter`() = checkByCode(""" + module 0x1::main { + struct S { x: u64 } + fun receiver(y: u64, self: &S): u64 { + self.x + y + } + fun test_call_styles(s: S): u64 { + s.receiver(&s) + //^ unresolved + } + } + """) } diff --git a/src/test/kotlin/org/move/lang/resolve/ResolveItemsProjectTest.kt b/src/test/kotlin/org/move/lang/resolve/ResolveItemsProjectTest.kt index f1999e040..bdff3b947 100644 --- a/src/test/kotlin/org/move/lang/resolve/ResolveItemsProjectTest.kt +++ b/src/test/kotlin/org/move/lang/resolve/ResolveItemsProjectTest.kt @@ -540,4 +540,77 @@ module 0x1::main { """) } } + + fun `test resolve vector method if stdlib vector module present`() = checkByFileTree { + namedMoveToml("MyPackage") + sources { + move("vector.move", """ + module 0x1::vector { + public native fun length(self: &vector): u8; + //X + } + """) + main(""" + module 0x1::main { + fun main() { + vector[1].length(); + //^ + } + } + """) + } + } + + fun `test resolve vector reference method if stdlib vector module present`() = checkByFileTree { + namedMoveToml("MyPackage") + sources { + move("vector.move", """ + module 0x1::vector { + public native fun length(self: &vector): u8; + //X + } + """) + main(""" + module 0x1::main { + fun main() { + (&vector[1]).length(); + //^ + } + } + """) + } + } + + fun `test vector method unresolved if no stdlib module`() = checkByFileTree { + namedMoveToml("MyPackage") + sources { + main(""" + module 0x1::main { + fun main() { + vector[1].length(); + //^ unresolved + } + } + """) + } + } + + fun `test vector method unresolved if address of vector module is different`() = checkByFileTree { + namedMoveToml("MyPackage") + sources { + move("vector.move", """ + module 0x2::vector { + public native fun length(self: &vector): u8; + } + """) + main(""" + module 0x1::main { + fun main() { + vector[1].length(); + //^ unresolved + } + } + """) + } + } } diff --git a/src/test/kotlin/org/move/lang/types/CallableTypeTest.kt b/src/test/kotlin/org/move/lang/types/CallableTypeTest.kt new file mode 100644 index 000000000..356565b55 --- /dev/null +++ b/src/test/kotlin/org/move/lang/types/CallableTypeTest.kt @@ -0,0 +1,69 @@ +package org.move.lang.types + +import org.intellij.lang.annotations.Language +import org.move.ide.presentation.text +import org.move.lang.core.psi.MvMethodCall +import org.move.lang.core.psi.ext.MvCallable +import org.move.lang.core.psi.ext.isMsl +import org.move.lang.core.types.infer.inference +import org.move.utils.tests.InlineFile +import org.move.utils.tests.base.findElementAndDataInEditor +import org.move.utils.tests.types.TypificationTestCase + +class CallableTypeTest: TypificationTestCase() { + fun `test infer method type`() = testMethodType(""" + module 0x1::main { + struct S { field: T } + fun receiver(self: &S, param: U): U { + param + } + fun main(s: S) { + s.receiver(1); + //^ fn(&S, integer) -> integer + } + } + """) + + fun `test infer method type not enough parameters`() = testMethodType(""" + module 0x1::main { + struct S { field: T } + fun receiver(self: &S, param: U): Z { + param + } + fun main(s: S) { + s.receiver(); + //^ fn(&S, ) -> ?Z + } + } + """) + + fun `test infer method type cannot autoborrow unknown function`() = testMethodType(""" + module 0x1::main { + struct S { field: T } + fun receiver(self: &mut S, param: U): U { + param + } + fun main(s: &S) { + s.receiver(1); + //^ fn(, ) -> + } + } + """) + + private fun testMethodType(@Language("Move") code: String) = testCallableType(code) + + private inline fun testCallableType(@Language("Move") code: String) { + InlineFile(myFixture, code, "main.move") + val (callable, data) = myFixture.findElementAndDataInEditor() + val expectedType = data.trim() + + val msl = callable.isMsl() + val inference = callable.inference(msl) ?: error("No inference at caret element") + + val actualType = inference.getCallableType(callable)?.text() ?: "null" + check(actualType == expectedType) { + "Type mismatch. \n" + + " expected: $expectedType, \n found: $actualType" + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/move/lang/types/ExpectedTypeTest.kt b/src/test/kotlin/org/move/lang/types/ExpectedTypeTest.kt index 2d6c13914..ad580d38c 100644 --- a/src/test/kotlin/org/move/lang/types/ExpectedTypeTest.kt +++ b/src/test/kotlin/org/move/lang/types/ExpectedTypeTest.kt @@ -230,4 +230,22 @@ class ExpectedTypeTest : TypificationTestCase() { } """ ) + + fun `test if block`() = testExpectedTyExpr(""" + module 0x1::main { + fun main(): u8 { + if (true) { my_ref } else { my_ref } + //^ u8 + } + } + """) + + fun `test else block`() = testExpectedTyExpr(""" + module 0x1::main { + fun main(): u8 { + if (true) { my_ref } else { my_ref } + //^ u8 + } + } + """) } diff --git a/src/test/kotlin/org/move/lang/types/ExpressionTypeInferenceTest.kt b/src/test/kotlin/org/move/lang/types/ExpressionTypeInferenceTest.kt index bc813d555..055e50d31 100644 --- a/src/test/kotlin/org/move/lang/types/ExpressionTypeInferenceTest.kt +++ b/src/test/kotlin/org/move/lang/types/ExpressionTypeInferenceTest.kt @@ -521,4 +521,44 @@ module 0x1::main { // } // """ // ) + + fun `test infer receiver style function type generic self`() = testExpr(""" + module 0x1::main { + struct S { field: T } + fun receiver(self: S): T { + self.field + } + fun main(s: S) { + s.receiver(); + //^ u8 + } + } + """) + + fun `test infer receiver style function type generic param`() = testExpr(""" + module 0x1::main { + struct S { field: u8 } + fun receiver(self: S, p: T): T { + p + } + fun main(s: S) { + s.receiver(1u8); + //^ u8 + } + } + """) + + fun `test method infer parameter type`() = testExpr(""" + module 0x1::main { + struct S { field: T } + fun receiver(self: &S, param: U): U { + param + } + fun main(s: S) { + let a = s.receiver(1); + a; + //^ integer + } + } + """) } diff --git a/src/test/resources/org/move/lang/parser/complete/expressions.move b/src/test/resources/org/move/lang/parser/complete/expressions.move index 36681b4f8..5cb0fce59 100644 --- a/src/test/resources/org/move/lang/parser/complete/expressions.move +++ b/src/test/resources/org/move/lang/parser/complete/expressions.move @@ -41,6 +41,12 @@ module M { fun dot() { bin.field1.field2; bin.field < 1; + bin.receiver_func(); + bin.field.call(); + + bin.call(); + bin.call(); + vector[1].length(); } spec dot { bin.field[1]; diff --git a/src/test/resources/org/move/lang/parser/complete/expressions.txt b/src/test/resources/org/move/lang/parser/complete/expressions.txt index 2c3f812bf..6e499445d 100644 --- a/src/test/resources/org/move/lang/parser/complete/expressions.txt +++ b/src/test/resources/org/move/lang/parser/complete/expressions.txt @@ -429,6 +429,112 @@ FILE MvLitExprImpl(LIT_EXPR) PsiElement(INTEGER_LITERAL)('1') PsiElement(;)(';') + PsiWhiteSpace('\n ') + MvExprStmtImpl(EXPR_STMT) + MvDotExprImpl(DOT_EXPR) + MvRefExprImpl(REF_EXPR) + MvPathImpl(PATH) + PsiElement(IDENTIFIER)('bin') + PsiElement(.)('.') + MvMethodCallImpl(METHOD_CALL) + PsiElement(IDENTIFIER)('receiver_func') + MvValueArgumentListImpl(VALUE_ARGUMENT_LIST) + PsiElement(()('(') + PsiElement())(')') + PsiElement(;)(';') + PsiWhiteSpace('\n ') + MvExprStmtImpl(EXPR_STMT) + MvDotExprImpl(DOT_EXPR) + MvDotExprImpl(DOT_EXPR) + MvRefExprImpl(REF_EXPR) + MvPathImpl(PATH) + PsiElement(IDENTIFIER)('bin') + PsiElement(.)('.') + MvStructDotFieldImpl(STRUCT_DOT_FIELD) + PsiElement(IDENTIFIER)('field') + PsiElement(.)('.') + MvMethodCallImpl(METHOD_CALL) + PsiElement(IDENTIFIER)('call') + MvValueArgumentListImpl(VALUE_ARGUMENT_LIST) + PsiElement(()('(') + PsiElement())(')') + PsiElement(;)(';') + PsiWhiteSpace('\n\n ') + MvExprStmtImpl(EXPR_STMT) + MvDotExprImpl(DOT_EXPR) + MvRefExprImpl(REF_EXPR) + MvPathImpl(PATH) + PsiElement(IDENTIFIER)('bin') + PsiElement(.)('.') + MvMethodCallImpl(METHOD_CALL) + PsiElement(IDENTIFIER)('call') + MvTypeArgumentListImpl(TYPE_ARGUMENT_LIST) + PsiElement(<)('<') + MvTypeArgumentImpl(TYPE_ARGUMENT) + MvPathTypeImpl(PATH_TYPE) + MvPathImpl(PATH) + PsiElement(IDENTIFIER)('T') + PsiElement(>)('>') + MvValueArgumentListImpl(VALUE_ARGUMENT_LIST) + PsiElement(()('(') + PsiElement())(')') + PsiElement(;)(';') + PsiWhiteSpace('\n ') + MvExprStmtImpl(EXPR_STMT) + MvDotExprImpl(DOT_EXPR) + MvRefExprImpl(REF_EXPR) + MvPathImpl(PATH) + PsiElement(IDENTIFIER)('bin') + PsiElement(.)('.') + MvMethodCallImpl(METHOD_CALL) + PsiElement(IDENTIFIER)('call') + MvTypeArgumentListImpl(TYPE_ARGUMENT_LIST) + PsiElement(<)('<') + MvTypeArgumentImpl(TYPE_ARGUMENT) + MvPathTypeImpl(PATH_TYPE) + MvPathImpl(PATH) + PsiElement(IDENTIFIER)('T') + PsiElement(,)(',') + PsiWhiteSpace(' ') + MvTypeArgumentImpl(TYPE_ARGUMENT) + MvPathTypeImpl(PATH_TYPE) + MvPathImpl(PATH) + PsiElement(IDENTIFIER)('U') + PsiElement(>)('>') + MvValueArgumentListImpl(VALUE_ARGUMENT_LIST) + PsiElement(()('(') + PsiElement())(')') + PsiElement(;)(';') + PsiWhiteSpace('\n ') + MvExprStmtImpl(EXPR_STMT) + MvDotExprImpl(DOT_EXPR) + MvVectorLitExprImpl(VECTOR_LIT_EXPR) + PsiElement(IDENTIFIER)('vector') + PsiElement(<)('<') + MvTypeArgumentImpl(TYPE_ARGUMENT) + MvPathTypeImpl(PATH_TYPE) + MvPathImpl(PATH) + PsiElement(IDENTIFIER)('u8') + PsiElement(>)('>') + MvVectorLitItemsImpl(VECTOR_LIT_ITEMS) + PsiElement([)('[') + MvLitExprImpl(LIT_EXPR) + PsiElement(INTEGER_LITERAL)('1') + PsiElement(])(']') + PsiElement(.)('.') + MvMethodCallImpl(METHOD_CALL) + PsiElement(IDENTIFIER)('length') + MvTypeArgumentListImpl(TYPE_ARGUMENT_LIST) + PsiElement(<)('<') + MvTypeArgumentImpl(TYPE_ARGUMENT) + MvPathTypeImpl(PATH_TYPE) + MvPathImpl(PATH) + PsiElement(IDENTIFIER)('u8') + PsiElement(>)('>') + MvValueArgumentListImpl(VALUE_ARGUMENT_LIST) + PsiElement(()('(') + PsiElement())(')') + PsiElement(;)(';') PsiWhiteSpace('\n ') PsiElement(})('}') PsiWhiteSpace('\n ') diff --git a/src/test/resources/org/move/lang/parser/partial/dot_exprs.move b/src/test/resources/org/move/lang/parser/partial/dot_exprs.move index f5d26a82f..1382112cf 100644 --- a/src/test/resources/org/move/lang/parser/partial/dot_exprs.move +++ b/src/test/resources/org/move/lang/parser/partial/dot_exprs.move @@ -1,9 +1,5 @@ module 0x1::dot_exprs { fun m() { - bin.field. call(); - - bin. call(); - bin. assert!(true, 1); bin. S { field: 1 }; diff --git a/src/test/resources/org/move/lang/parser/partial/dot_exprs.txt b/src/test/resources/org/move/lang/parser/partial/dot_exprs.txt index 55706719f..22323cb78 100644 --- a/src/test/resources/org/move/lang/parser/partial/dot_exprs.txt +++ b/src/test/resources/org/move/lang/parser/partial/dot_exprs.txt @@ -21,46 +21,6 @@ FILE MvCodeBlockImpl(CODE_BLOCK) PsiElement({)('{') PsiWhiteSpace('\n ') - MvExprStmtImpl(EXPR_STMT) - MvDotExprImpl(DOT_EXPR) - MvDotExprImpl(DOT_EXPR) - MvRefExprImpl(REF_EXPR) - MvPathImpl(PATH) - PsiElement(IDENTIFIER)('bin') - PsiElement(.)('.') - MvStructDotFieldImpl(STRUCT_DOT_FIELD) - PsiElement(IDENTIFIER)('field') - PsiElement(.)('.') - PsiErrorElement:IDENTIFIER expected, got 'call' - - PsiWhiteSpace(' ') - MvExprStmtImpl(EXPR_STMT) - MvCallExprImpl(CALL_EXPR) - MvPathImpl(PATH) - PsiElement(IDENTIFIER)('call') - MvValueArgumentListImpl(VALUE_ARGUMENT_LIST) - PsiElement(()('(') - PsiElement())(')') - PsiElement(;)(';') - PsiWhiteSpace('\n\n ') - MvExprStmtImpl(EXPR_STMT) - MvDotExprImpl(DOT_EXPR) - MvRefExprImpl(REF_EXPR) - MvPathImpl(PATH) - PsiElement(IDENTIFIER)('bin') - PsiElement(.)('.') - PsiErrorElement:IDENTIFIER expected, got 'call' - - PsiWhiteSpace(' ') - MvExprStmtImpl(EXPR_STMT) - MvCallExprImpl(CALL_EXPR) - MvPathImpl(PATH) - PsiElement(IDENTIFIER)('call') - MvValueArgumentListImpl(VALUE_ARGUMENT_LIST) - PsiElement(()('(') - PsiElement())(')') - PsiElement(;)(';') - PsiWhiteSpace('\n\n ') MvExprStmtImpl(EXPR_STMT) MvDotExprImpl(DOT_EXPR) MvRefExprImpl(REF_EXPR) @@ -115,12 +75,11 @@ FILE PsiElement(;)(';') PsiWhiteSpace('\n\n ') MvExprStmtImpl(EXPR_STMT) - MvDotExprImpl(DOT_EXPR) - MvRefExprImpl(REF_EXPR) - MvPathImpl(PATH) - PsiElement(IDENTIFIER)('bin') - PsiElement(.)('.') - PsiErrorElement:';' expected, got 'vector' + MvRefExprImpl(REF_EXPR) + MvPathImpl(PATH) + PsiElement(IDENTIFIER)('bin') + PsiElement(.)('.') + PsiErrorElement:'vector' unexpected PsiWhiteSpace(' ') MvExprStmtImpl(EXPR_STMT) diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/ActionMenuFixture.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/ActionMenuFixture.kt new file mode 100644 index 000000000..b4de042bc --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/ActionMenuFixture.kt @@ -0,0 +1,32 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.move.ui.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.waitFor + +fun RemoteRobot.actionMenu(text: String): ActionMenuFixture { + val xpath = byXpath("text '$text'", "//div[@class='ActionMenu' and @text='$text']") + waitFor { + findAll(xpath).isNotEmpty() + } + return findAll(xpath).first() +} + +fun RemoteRobot.actionMenuItem(text: String): ActionMenuItemFixture { + val xpath = byXpath("text '$text'", "//div[@class='ActionMenuItem' and @text='$text']") + waitFor { + findAll(xpath).isNotEmpty() + } + return findAll(xpath).first() +} + +@FixtureName("ActionMenu") +class ActionMenuFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : ComponentFixture(remoteRobot, remoteComponent) + +@FixtureName("ActionMenuItem") +class ActionMenuItemFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : ComponentFixture(remoteRobot, remoteComponent) \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt new file mode 100644 index 000000000..b43f33ca7 --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt @@ -0,0 +1,53 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.move.ui.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import org.move.ui.fixtures.DialogFixture.Companion +import java.time.Duration + +fun ContainerFixture.dialog( + title: String, + timeout: Duration = Duration.ofSeconds(20), + function: DialogFixture.() -> Unit = {} +): DialogFixture = step("Search for dialog with title $title") { + find(Companion.byTitle(title), timeout).apply(function) +} + +@FixtureName("Dialog") +open class DialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): CommonContainerFixture(remoteRobot, remoteComponent) { + + companion object { + @JvmStatic + fun byTitle(title: String) = byXpath("title $title", "//div[@title='$title' and @class='MyDialog']") + } + + val title: String + get() = callJs("component.getTitle();") + + // component is DialogWrapperPeerImpl.MyDialog + fun doOKAction() = runJs("""component.getDialogWrapper().performOKAction(); """, runInEdt = true) + fun doCancelAction() = runJs("""component.getDialogWrapper().doCancelAction(); """, runInEdt = true) +} + +class SettingsDialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): + DialogFixture(remoteRobot, remoteComponent) { + + fun configurableEditor( + timeout: Duration = Duration.ofSeconds(20), + function: CommonContainerFixture.() -> Unit = {} + ) = + find(byXpath("//div[@class='ConfigurableEditor']"), timeout).apply(function) +} \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt new file mode 100644 index 000000000..f3f464550 --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt @@ -0,0 +1,186 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.move.ui.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.Locator +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.waitFor +import java.time.Duration + +fun RemoteRobot.ideaFrame(function: IdeaFrame.() -> Unit) { + find(timeout = Duration.ofSeconds(10)).apply(function) +} + +fun RemoteRobot.executeCmd(cmd: String, workDir: String): String = + this.callJs( + """ + importClass(java.lang.StringBuilder) + importPackage(java.io) + + let result = null; + const builder = new StringBuilder(); + const pBuilder = new ProcessBuilder(${ + cmd.split(" ").joinToString(separator = "\", \"", prefix = "\"", postfix = "\"") + }) + .directory(File("$workDir")) + .redirectErrorStream(true); + let p; + try { + let s; + p = pBuilder.start(); + const br = new BufferedReader(new InputStreamReader(p.getInputStream())); + while ((s = br.readLine()) != null) { + builder.append(s + "\n") + } + p.waitFor(); + result = builder.toString(); + } catch (e) { + result = e.getMessage().toString() + } finally { + if (p) { p.destroy(); } + } + result; + """ + ) + + +fun ContainerFixture.findIsVisible(locator: Locator): Boolean = + this.findAll(locator).isNotEmpty() + +fun ContainerFixture.findIsNotVisible(locator: Locator): Boolean = !findIsVisible(locator) + +@FixtureName("Idea frame") +@DefaultXpath("IdeFrameImpl type", "//div[@class='IdeFrameImpl']") +class IdeaFrame( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): + CommonContainerFixture(remoteRobot, remoteComponent) { + + val projectViewTree + get() = find(byXpath("ProjectViewTree", "//div[@class='ProjectViewTree']")) + + val projectName + get() = step("Get project name") { return@step callJs("component.getProject().getName()") } + + val menuBar: JMenuBarFixture + get() = step("Menu...") { + return@step remoteRobot.find( + JMenuBarFixture::class.java, JMenuBarFixture.byType(), Duration.ofSeconds(5) + ) + } + + val inlineProgressPanel get() = find(byXpath("//div[@class='InlineProgressPanel']")) + +// private fun openSettingsDialog() { +// if (!remoteRobot.isMac()) { +// waitFor { +// findAll( +// Locators.byTypeAndProperties(JMenu::class.java, Locators.XpathProperty.ACCESSIBLE_NAME to "File") +// ) +// .isNotEmpty() +// } +// } +// menuBar.select("File", "Settings...") +// waitFor { +// findAll(DialogFixture.byTitle("Settings")).isNotEmpty() +// } +// } + + fun settingsDialog(function: SettingsDialogFixture.() -> Unit) = + find( + locator = DialogFixture.byTitle("Settings"), + timeout = Duration.ofSeconds(20) + ).apply(function) +// dialog("Settings", function = function) + + fun SettingsDialogFixture.selectMoveSettings() { + val settingsTreeView = find(byXpath("//div[@class='SettingsTreeView']")) + settingsTreeView.findText("Languages & Frameworks").click() + configurableEditor { + find(byXpath("//div[@text='Move Language']")).click() + } + } + + fun moveSettings(function: MoveSettingsPanelFixture.() -> Unit) { + // show settings dialog + remoteRobot.commonSteps.invokeAction("ShowSettings") + waitFor { + findAll(DialogFixture.byTitle("Settings")).isNotEmpty() + } + remoteRobot.commonSteps.waitMs(300) + + settingsDialog { + selectMoveSettings() + configurableEditor { + moveSettingsPanel(function = function) + } + doOKAction() + } + } + + @JvmOverloads + fun dumbAware(timeout: Duration = Duration.ofMinutes(5), function: () -> Unit) { + step("Wait for smart mode") { + waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { + runCatching { isDumbMode().not() }.getOrDefault(false) + } + function() + step("..wait for smart mode again") { + waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { + isDumbMode().not() + } + } + } + } + + fun isDumbMode(): Boolean { + return callJs( + """ + const frameHelper = com.intellij.openapi.wm.impl.ProjectFrameHelper.getFrameHelper(component) + if (frameHelper) { + const project = frameHelper.getProject() + project ? com.intellij.openapi.project.DumbService.isDumb(project) : true + } else { + true + } + """, true + ) + } + + fun openFile(path: String) { + runJs( + """ + importPackage(com.intellij.openapi.fileEditor) + importPackage(com.intellij.openapi.vfs) + importPackage(com.intellij.openapi.wm.impl) + importClass(com.intellij.openapi.application.ApplicationManager) + + const path = '$path' + const frameHelper = ProjectFrameHelper.getFrameHelper(component) + if (frameHelper) { + const project = frameHelper.getProject() + const projectPath = project.getBasePath() + const file = LocalFileSystem.getInstance().findFileByPath(projectPath + '/' + path) + const openFileFunction = new Runnable({ + run: function() { + FileEditorManager.getInstance(project).openTextEditor( + new OpenFileDescriptor( + project, + file + ), true + ) + } + }) + ApplicationManager.getApplication().invokeLater(openFileFunction) + } + """, true + ) + } + + fun closeAllEditorTabs() = remoteRobot.commonSteps.invokeAction("CloseAllEditors") +} \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/MoveSettingsPanelFixture.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/MoveSettingsPanelFixture.kt new file mode 100644 index 000000000..1159fcbd7 --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/MoveSettingsPanelFixture.kt @@ -0,0 +1,40 @@ +package org.move.ui.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import java.time.Duration + +fun ContainerFixture.moveSettingsPanel( + timeout: Duration = Duration.ofSeconds(20), + function: MoveSettingsPanelFixture.() -> Unit = {} +): MoveSettingsPanelFixture = + step("Search for Move settings panel") { + find(MoveSettingsPanelFixture::class.java, timeout).apply(function) + } + +@FixtureName("MoveSettingsPanel") +@DefaultXpath("DialogPanel type", "//div[@class='DialogPanel']") +class MoveSettingsPanelFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): + CommonContainerFixture(remoteRobot, remoteComponent) { + + val aptosRadioButton get() = radioButton("Aptos") + val suiRadioButton get() = radioButton("Sui") + + val bundledRadioButton get() = radioButton("Bundled") + val localRadioButton get() = radioButton("Local") + + val versionLabel get() = jLabel(byXpath("//div[@class='VersionLabel']"), timeout = Duration.ofSeconds(1)) + + val localPathTextField + get() = + textFieldWithBrowseButton(byXpath("//div[@class='TextFieldWithBrowseButton']")) +} \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/TextFieldWithBrowseButtonFixture.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/TextFieldWithBrowseButtonFixture.kt new file mode 100644 index 000000000..b86a2a0af --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/TextFieldWithBrowseButtonFixture.kt @@ -0,0 +1,55 @@ +package org.move.ui.fixtures + +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.fixtures.JLabelFixture +import com.intellij.remoterobot.search.locators.Locator +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.RelativeLocators +import java.time.Duration + +fun CommonContainerFixture.textFieldWithBrowseButton( + locator: Locator, + timeout: Duration = Duration.ofSeconds(5) +) = find(locator, timeout) + +fun CommonContainerFixture.textFieldWithBrowseButton(labelText: String): TextFieldWithBrowseButtonFixture { + val locator = TextFieldWithBrowseButtonFixture.byLabel(jLabel(labelText)) + return textFieldWithBrowseButton(locator) +} + +class TextFieldWithBrowseButtonFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): + ComponentFixture(remoteRobot, remoteComponent) { + + companion object { +// @JvmStatic +// fun byType() = Locators.byType(TextFieldWithBrowseButton::class.java) + + @JvmStatic + fun byLabel(fixture: JLabelFixture): Locator { + return RelativeLocators.byLabel(fixture) + } + } + + var text: String + set(value) = step("Set text '$value'") { + runJs( + "JTextComponentFixture(robot, component.getTextField())" + + ".setText('${value.replace("\\", "\\\\")}')" + ) + } + get() = step("Get text") { + callJs("component.getTextField().getText() || ''", true) + } + + val isEnabled: Boolean + get() = step("..is enabled?") { + callJs("component.isEnabled()", true) + } +} \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt new file mode 100644 index 000000000..64fde2c46 --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt @@ -0,0 +1,240 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.move.ui.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.SearchContext +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.Locator +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.steps.CommonSteps +import com.intellij.remoterobot.steps.Step +import com.intellij.remoterobot.steps.StepParameter +import com.intellij.remoterobot.utils.Locators +import com.intellij.remoterobot.utils.keyboard +import com.intellij.remoterobot.utils.repeatInTime +import com.intellij.remoterobot.utils.waitFor +import com.intellij.ui.dsl.builder.components.DslLabel +import java.io.File +import java.nio.file.Path +import java.time.Duration + +val RemoteRobot.commonSteps get() = CommonSteps(this) + +fun RemoteRobot.welcomeFrame(function: WelcomeFrame.() -> Unit) { + find(WelcomeFrame::class.java, Duration.ofSeconds(10)).apply(function) +} + +fun RemoteRobot.openOrImportProject(projectDir: File) = openOrImportProject(projectDir.toPath()) + +fun RemoteRobot.openOrImportProject(absolutePath: Path) = openOrImportProject(absolutePath.toString()) + +@Step("Open or import project", "Open or import project '{1}'") +fun RemoteRobot.openOrImportProject(@StepParameter("Project absolute path", "") absolutePath: String) { + this.runJs( + """ + importClass(com.intellij.openapi.application.ApplicationManager) + importClass(com.intellij.ide.impl.ProjectUtil) + importClass(com.intellij.ide.impl.OpenProjectTask) + + let task + try { + task = OpenProjectTask.build() + } catch(e) { + task = OpenProjectTask.newProject() + } + + const path = new java.io.File("$absolutePath").toPath() + const openProjectFunction = new Runnable({ + run: function() { + ProjectUtil.openOrImport(path, task) + } + }) + + ApplicationManager.getApplication().invokeLater(openProjectFunction) + """, runInEdt = true + ) + + // wait for 5 seconds or till isDumbMode == false is achieved + repeatInTime( + duration = Duration.ofSeconds(5), + interval = Duration.ofSeconds(1), + ) { + !this.commonSteps.isDumbMode() + } +} + +fun RemoteRobot.closeProject() { + CommonSteps(this).closeProject() + waitFor(description = "Wait for the Welcome screen to appear") { + findAll().isNotEmpty() + } +} + +@Step("Remove project from recents", "Remove project from recents") +fun RemoteRobot.removeLastRecentProject() { + this.runJs( + """ + importClass(com.intellij.openapi.application.ApplicationManager) + importClass(com.intellij.ide.RecentProjectsManagerBase) + + const removeRecentProjectFunction = new Runnable({ + run: function() { + const recentsProjectsManager = RecentProjectsManagerBase.getInstanceEx(); + const lastProjectPath = recentsProjectsManager.getLastOpenedProject(); + if (lastProjectPath != null) { + recentsProjectsManager.removePath(lastProjectPath); + } + } + }) + + ApplicationManager.getApplication().invokeLater(removeRecentProjectFunction) + """, runInEdt = true + ) +} + +fun RemoteRobot.removeProjectFromRecents(absolutePath: Path) = removeProjectFromRecents(absolutePath.toString()) + +@Step("Remove from recents", "Remove '{1}' from recents") +fun RemoteRobot.removeProjectFromRecents(@StepParameter("Project absolute path", "") absolutePath: String) { + this.runJs( + """ + importClass(com.intellij.openapi.application.ApplicationManager) + importClass(com.intellij.ide.RecentProjectsManager) + + const projectPath = new java.io.File("$absolutePath").toPath() + const removeRecentProjectFunction = new Runnable({ + run: function() { + RecentProjectsManager.getInstance().removePath(projectPath) + } + }) + + ApplicationManager.getApplication().invokeLater(removeRecentProjectFunction) + """, runInEdt = true + ) +} + +fun SearchContext.findOrNull(type: Class, locator: Locator) = + findAll(type, locator).firstOrNull() + +@FixtureName("Welcome Frame") +@DefaultXpath("type", "//div[@class='FlatWelcomeFrame']") +class WelcomeFrame( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): + CommonContainerFixture(remoteRobot, remoteComponent) { + + val newProjectButton: JButtonFixture + get() { + val cleanStartupButton = + findOrNull(JButtonFixture::class.java, byXpath("//div[@defaulticon='createNewProjectTab.svg']")) + if (cleanStartupButton != null) { + return cleanStartupButton + } + + return button( + byXpath("//div[@class='JBOptionButton' and @text='New Project']"), + timeout = Duration.ofSeconds(2) + ) + } + + val openProjectButton: JButtonFixture + get() { + val cleanStartupButton = + findOrNull(JButtonFixture::class.java, byXpath("//div[@defaulticon='open.svg']")) + if (cleanStartupButton != null) { + return cleanStartupButton + } + + return button( + byXpath("//div[@class='JBOptionButton' and @text='Open']"), + timeout = Duration.ofSeconds(2) + ) + } + + + fun selectNewProjectType(type: String) { + newProjectButton.click() + projectTypesList.findText(type).click() + } + + val validationLabel: JLabelFixture? + get() = + findOrNull(JLabelFixture::class.java, byXpath("//div[@defaulticon='lightning.svg']")) + + val projectTypesList + get() = find(ComponentFixture::class.java, byXpath("//div[@class='JBList']")) + + val projectLocationTextField get() = textFieldWithBrowseButton("Location:") +// textField(byXpath("//div[@accessiblename='Location:' and @class='TextFieldWithBrowseButton']")) + + val createButton get() = button("Create") + val cancelButton get() = button("Cancel") + + @Suppress("UnstableApiUsage") + val bundledAptosUnsupportedComment: ComponentFixture? + get() { + val labelText = "is not available for this platform" + return findOrNull( + ComponentFixture::class.java, + Locators.byTypeAndPropertiesContains( + DslLabel::class.java, + Locators.XpathProperty.TEXT to labelText + ) + ) + } + + fun openProjectAt(path: Path) { + openProjectButton.click() + dialog("Open File or Project") { + val fileTree = jTree() + fileTree.collapsePath("/") + fileTree.clickPath("/") + + val pathTextField = textField(byXpath("//div[@class='BorderlessTextField']")) + pathTextField.click() + + val absPath = path.toAbsolutePath().toString().drop(1) + keyboard { + enterText(absPath, 10) + } + + val treePath = arrayOf("/") + absPath.split(File.separator).toTypedArray() + waitFor { + fileTree.isPathExists(*treePath) + } + fileTree.clickPath(*treePath) + + Thread.sleep(300) + button("OK").click() + } + } + + fun openRecentProject(projectName: String) { + findText(projectName).click() + } + + fun removeProjectFromRecents(projectName: String) { + findText(projectName).rightClick() + jPopupMenu().menuItem("Remove from Recent Projects…").click() + dialog("Remove Recent Project") { + button("Remove").click() + } + } + + val createNewProjectLink + get() = actionLink( + byXpath( + "New Project", + "//div[(@class='MainButton' and @text='New Project') or (@accessiblename='New Project' and @class='JButton')]" + ) + ) + + val moreActions + get() = button(byXpath("More Action", "//div[@accessiblename='More Actions']")) + + val heavyWeightPopup + get() = remoteRobot.find(ComponentFixture::class.java, byXpath("//div[@class='HeavyWeightWindow']")) +} \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/utils/RemoteRobotExtension.kt b/ui-tests/src/main/kotlin/org/move/ui/utils/RemoteRobotExtension.kt new file mode 100644 index 000000000..d6f85d2ad --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/utils/RemoteRobotExtension.kt @@ -0,0 +1,144 @@ +package org.move.ui.utils + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.search.locators.byXpath +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.logging.HttpLoggingInterceptor +import org.junit.jupiter.api.extension.AfterTestExecutionCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.ParameterContext +import org.junit.jupiter.api.extension.ParameterResolver +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.io.File +import java.lang.reflect.Method +import javax.imageio.ImageIO + +class RemoteRobotExtension: AfterTestExecutionCallback, ParameterResolver { + private val url: String = System.getProperty("remote-robot-url") ?: "http://127.0.0.1:8082" + private val remoteRobot: RemoteRobot = + if (System.getProperty("debug-retrofit")?.equals("enable") == true) { + val interceptor: HttpLoggingInterceptor = HttpLoggingInterceptor().apply { + this.level = HttpLoggingInterceptor.Level.BODY + } + val client = OkHttpClient.Builder().apply { + this.addInterceptor(interceptor) + }.build() + RemoteRobot(url, client) + } else { + RemoteRobot(url) + } + private val client = OkHttpClient() + + override fun supportsParameter( + parameterContext: ParameterContext?, + extensionContext: ExtensionContext? + ): Boolean { + return parameterContext?.parameter?.type?.equals(RemoteRobot::class.java) ?: false + } + + override fun resolveParameter( + parameterContext: ParameterContext?, + extensionContext: ExtensionContext? + ): Any { + return remoteRobot + } + + override fun afterTestExecution(context: ExtensionContext?) { + val testMethod: Method = + context?.requiredTestMethod ?: throw IllegalStateException("test method is null") + val testMethodName = testMethod.name + val testFailed: Boolean = context.executionException?.isPresent ?: false + if (testFailed) { +// saveScreenshot(testMethodName) + saveIdeaFrames(testMethodName) + saveHierarchy(testMethodName) + } + } + + private fun saveScreenshot(testName: String) { + fetchScreenShot().save(testName) + } + + private fun saveHierarchy(testName: String) { + val hierarchySnapshot = + saveFile(url, "build/reports", "hierarchy-$testName.html") + if (File("build/reports/styles.css").exists().not()) { + saveFile("$url/styles.css", "build/reports", "styles.css") + } + println("Hierarchy snapshot: ${hierarchySnapshot.absolutePath}") + } + + private fun saveFile(url: String, folder: String, name: String): File { + val response = client.newCall(Request.Builder().url(url).build()).execute() + return File(folder).apply { + mkdirs() + }.resolve(name).apply { + writeText(response.body?.string() ?: "") + } + } + + private fun BufferedImage.save(name: String) { + val bytes = ByteArrayOutputStream().use { b -> + ImageIO.write(this, "png", b) + b.toByteArray() + } + File("build/reports").apply { mkdirs() }.resolve("$name.png").writeBytes(bytes) + } + + private fun saveIdeaFrames(testName: String) { + remoteRobot.findAll(byXpath("//div[@class='IdeFrameImpl']")) + .forEachIndexed { n, frame -> + val pic = try { + frame.callJs( + """ + importPackage(java.io) + importPackage(javax.imageio) + importPackage(java.awt.image) + const screenShot = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_ARGB); + component.paint(screenShot.getGraphics()) + let pictureBytes; + const baos = new ByteArrayOutputStream(); + try { + ImageIO.write(screenShot, "png", baos); + pictureBytes = baos.toByteArray(); + } finally { + baos.close(); + } + pictureBytes; + """, true + ) + } catch (e: Throwable) { + e.printStackTrace() + throw e + } + pic.inputStream().use { + ImageIO.read(it) + }.save(testName + "_" + n) + } + } + + private fun fetchScreenShot(): BufferedImage { + return remoteRobot.callJs( + """ + importPackage(java.io) + importPackage(javax.imageio) + const screenShot = new java.awt.Robot().createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())); + let pictureBytes; + const baos = new ByteArrayOutputStream(); + try { + ImageIO.write(screenShot, "png", baos); + pictureBytes = baos.toByteArray(); + } finally { + baos.close(); + } + pictureBytes; + """ + ).inputStream().use { + ImageIO.read(it) + } + } +} + diff --git a/ui-tests/src/main/kotlin/org/move/ui/utils/StepsLogger.kt b/ui-tests/src/main/kotlin/org/move/ui/utils/StepsLogger.kt new file mode 100644 index 000000000..84d36f90e --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/utils/StepsLogger.kt @@ -0,0 +1,17 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.move.ui.utils + +import com.intellij.remoterobot.stepsProcessing.StepLogger +import com.intellij.remoterobot.stepsProcessing.StepWorker + +object StepsLogger { + private var initializaed = false + @JvmStatic + fun init() { + if (initializaed.not()) { + StepWorker.registerProcessor(StepLogger()) + initializaed = true + } + } +} \ No newline at end of file diff --git a/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt b/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt new file mode 100644 index 000000000..e11924d5d --- /dev/null +++ b/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt @@ -0,0 +1,289 @@ +package org.move.ui + +import com.intellij.openapi.util.io.toCanonicalPath +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.waitFor +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.io.TempDir +import org.move.ui.fixtures.* +import org.move.ui.utils.RemoteRobotExtension +import org.move.ui.utils.StepsLogger +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import java.time.Duration + +const val APTOS_LOCAL_PATH = "/home/mkurnikov/bin/aptos" +const val SUI_LOCAL_PATH = "/home/mkurnikov/bin/sui" + +class NewProjectTest: UiTestBase() { + @Test + fun `new project validation`(robot: RemoteRobot) = with(robot) { + welcomeFrame { + selectNewProjectType("Move") + } + + // check radio buttons behaviour + welcomeFrame { + moveSettingsPanel { + aptosRadioButton.select() + + assert(bundledAptosUnsupportedComment == null) + + bundledRadioButton.select() + assert(bundledRadioButton.isSelected()) + assert(!localPathTextField.isEnabled) { "Local path should be disabled if Bundled is selected" } + + waitFor(interval = Duration.ofSeconds(1)) { versionLabel.value.contains("aptos") } + assert(validationLabel == null) + + localRadioButton.select() + assert(localRadioButton.isSelected()) + + assert(localPathTextField.isEnabled) { "Local path should be enabled if Local is selected" } + localPathTextField.text = "" + + waitFor { versionLabel.value.contains("N/A") } + waitFor { validationLabel?.value == "Invalid path to Aptos executable" } + } + } + + welcomeFrame { + moveSettingsPanel { + suiRadioButton.select() + + localPathTextField.text = "" + + waitFor { versionLabel.value.contains("N/A") } + waitFor { validationLabel?.value == "Invalid path to Sui executable" } + } + } + + welcomeFrame { + cancelButton.click() + } + } + + @Test + fun `create new aptos project with bundled cli`(robot: RemoteRobot) = with(robot) { + welcomeFrame { + selectNewProjectType("Move") + } + + val projectName = "aptos_project" + val projectPath = Paths.get(tempFolder.canonicalPath, projectName).toCanonicalPath() + + welcomeFrame { + moveSettingsPanel { + aptosRadioButton.select() + bundledRadioButton.select() + + assert(projectLocationTextField.isEnabled) + projectLocationTextField.text = projectPath + } + createButton.click() + } + + ideaFrame { + assert(textEditor().editor.text.contains("[dependencies.AptosFramework]")) + + moveSettings { + assert(aptosRadioButton.isSelected()) + assert(!suiRadioButton.isSelected()) + + assert(bundledRadioButton.isSelected()) + assert(!localRadioButton.isSelected()) + assert(!localPathTextField.isEnabled) + } + } + } + + @Test + fun `create new aptos project with local cli`(robot: RemoteRobot) = with(robot) { + welcomeFrame { + selectNewProjectType("Move") + } + + val projectName = "aptos_project" + val projectPath = Paths.get(tempFolder.canonicalPath, projectName).toCanonicalPath() + + welcomeFrame { + moveSettingsPanel { + aptosRadioButton.select() + + localRadioButton.select() + localPathTextField.text = APTOS_LOCAL_PATH + + projectLocationTextField.text = projectPath + } + createButton.click() + } + + ideaFrame { + assert(textEditor().editor.text.contains("[dependencies.AptosFramework]")) + + moveSettings { + assert(aptosRadioButton.isSelected()) + assert(!suiRadioButton.isSelected()) + + assert(!bundledRadioButton.isSelected()) + assert(localRadioButton.isSelected()) + + assert(localPathTextField.text == APTOS_LOCAL_PATH) + } + } + } + + @Test + fun `create new sui project`(robot: RemoteRobot) = with(robot) { + welcomeFrame { + selectNewProjectType("Move") + } + + val projectName = "sui_project" + val projectPath = Paths.get(tempFolder.canonicalPath, projectName).toCanonicalPath() + + welcomeFrame { + moveSettingsPanel { + suiRadioButton.select() + localPathTextField.text = SUI_LOCAL_PATH + projectLocationTextField.text = projectPath + } + createButton.click() + } + + ideaFrame { + assert(textEditor().editor.text.contains("https://github.com/MystenLabs/sui.git")) + + moveSettings { + assert(!aptosRadioButton.isSelected()) + assert(suiRadioButton.isSelected()) + assert(localPathTextField.text == SUI_LOCAL_PATH) + } + } + } + + @Test + fun `import existing aptos package`(robot: RemoteRobot) = with(robot) { + copyExamplePackageToTempFolder("aptos_package") + + val tempPackagePath = tempFolder.toPath().resolve("aptos_package") + openOrImportProject(tempPackagePath) + + ideaFrame { + val textEditor = textEditor() + assert(textEditor.editor.filePath == tempPackagePath.resolve("Move.toml").toString()) + } + + ideaFrame { + moveSettings { + assert(aptosRadioButton.isSelected()) + assert(!suiRadioButton.isSelected()) + } + } + } + + @Test + fun `import existing sui package`(robot: RemoteRobot) = with(robot) { + copyExamplePackageToTempFolder("sui_package") + + val projectPath = tempFolder.toPath().resolve("sui_package") + openOrImportProject(projectPath) + + ideaFrame { + val textEditor = textEditor() + assert(textEditor.editor.filePath == projectPath.resolve("Move.toml").toString()) + } + + ideaFrame { + moveSettings { + assert(!aptosRadioButton.isSelected()) + assert(suiRadioButton.isSelected()) + } + } + } + + @Test + fun `explicit sui blockchain setting should retain even if wrong`(robot: RemoteRobot) = with(robot) { + copyExamplePackageToTempFolder("aptos_package") + + // opens as Aptos package + val projectPath = tempFolder.toPath().resolve("aptos_package") + openOrImportProject(projectPath) + + // mark project as Sui + ideaFrame { + moveSettings { + suiRadioButton.select() + } + } + + // reopen project to see that no ProjectActivity or OpenProcessor changed the setting + closeProject() + openOrImportProject(projectPath) + + ideaFrame { + moveSettings { + assert(!aptosRadioButton.isSelected()) + assert(suiRadioButton.isSelected()) + } + } + } + + @Test + fun `no move toml opened after reopen from new project`(robot: RemoteRobot) = with(robot) { + welcomeFrame { + selectNewProjectType("Move") + } + + val projectName = "aptos_project" + val projectPath = Paths.get(tempFolder.canonicalPath, projectName).toCanonicalPath() + + welcomeFrame { + moveSettingsPanel { + aptosRadioButton.select() + bundledRadioButton.select() + + assert(projectLocationTextField.isEnabled) + projectLocationTextField.text = projectPath + } + createButton.click() + } + + ideaFrame { closeAllEditorTabs() } + closeProject() + openOrImportProject(projectPath) + + ideaFrame { + assert(textEditors().isEmpty()) + } + } + + @Test + fun `no move toml opened after reopen from existing project import`(robot: RemoteRobot) = with(robot) { + copyExamplePackageToTempFolder("aptos_package") + + // opens as Aptos package + val projectPath = tempFolder.toPath().resolve("aptos_package") + openOrImportProject(projectPath) + + ideaFrame { closeAllEditorTabs() } + closeProject() + openOrImportProject(projectPath) + + ideaFrame { + assert(textEditors().isEmpty()) + } + } + + // TODO +// @Test +// fun `no default compile configuration should be created in pycharm`(robot: RemoteRobot) = with(robot) { +// +// +// } +} \ No newline at end of file diff --git a/ui-tests/src/test/kotlin/org/move/ui/ToolWindowTest.kt b/ui-tests/src/test/kotlin/org/move/ui/ToolWindowTest.kt new file mode 100644 index 000000000..a94be5d7c --- /dev/null +++ b/ui-tests/src/test/kotlin/org/move/ui/ToolWindowTest.kt @@ -0,0 +1,102 @@ +package org.move.ui + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.JTreeFixture +import com.intellij.remoterobot.search.locators.byXpath +import org.junit.jupiter.api.Test +import org.move.ui.fixtures.* + +class ProjectsTreeFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): + JTreeFixture(remoteRobot, remoteComponent) + +class ToolWindowTest: UiTestBase() { + @Test + fun `aptos tool window not available if move project has no manifest`(robot: RemoteRobot) = with(robot) { + val projectPath = + copyExamplePackageToTempFolder("empty_move_package") + openOrImportProject(projectPath) + + // TODO: change into proper JS call to ToolWindowManager + ideaFrame { + val rightStripe = + find(byXpath("//div[@accessiblename='Right Stripe']")) + assert(rightStripe.findIsNotVisible(byXpath("//div[@text='Aptos']"))) + } + } + + @Test + fun `aptos tool window for sui project`(robot: RemoteRobot) = with(robot) { + val projectPath = + copyExamplePackageToTempFolder("sui_package") + openOrImportProject(projectPath) + + // TODO: change into proper JS call to ToolWindowManager + ideaFrame { + val rightStripe = + find(byXpath("//div[@accessiblename='Right Stripe']")) + assert(rightStripe.findIsVisible(byXpath("//div[@text='Aptos']"))) + } + } + + @Test + fun `aptos tool window for aptos project`(robot: RemoteRobot) = with(robot) { + val projectPath = + copyExamplePackageToTempFolder("aptos_package") + openOrImportProject(projectPath) + + // TODO: change into proper JS call to ToolWindowManager + ideaFrame { + val rightStripe = + find(byXpath("//div[@accessiblename='Right Stripe']")) + val aptosStripeButton = rightStripe.find(byXpath("//div[@text='Aptos']")) + aptosStripeButton.click() + robot.commonSteps.waitMs(500) + + val projectsTree = find(byXpath("//div[@class='MoveProjectsTree']")) + projectsTree.expandAll() + robot.commonSteps.waitMs(500) + + val topLevelsPaths = + projectsTree.collectExpandedPaths().filter { it.path.size <= 3 }.map { it.path } + assert( + topLevelsPaths == listOf( + listOf("MyAptosPackage"), + listOf("MyAptosPackage", "Dependencies"), + listOf("MyAptosPackage", "Dependencies", "MoveStdlib"), + listOf("MyAptosPackage", "Dependencies", "AptosStdlib"), + listOf("MyAptosPackage", "Dependencies", "AptosFramework"), + ) + ) + } + +// ideaFrame { +// moveSettings { +// suiRadioButton.select() +// } +// } +// +// ideaFrame { +// val rightStripe = +// find(byXpath("//div[@accessiblename='Right Stripe']")) +// assert(rightStripe.findIsNotVisible(byXpath("//div[@text='Aptos']"))) +// } +// +// ideaFrame { +// moveSettings { +// aptosRadioButton.select() +// } +// } +// +// ideaFrame { +// val rightStripe = +// find(byXpath("//div[@accessiblename='Right Stripe']")) +// assert(rightStripe.findIsVisible(byXpath("//div[@text='Aptos']"))) +// } + } +} \ No newline at end of file diff --git a/ui-tests/src/test/kotlin/org/move/ui/UiTestBase.kt b/ui-tests/src/test/kotlin/org/move/ui/UiTestBase.kt new file mode 100644 index 000000000..de5831480 --- /dev/null +++ b/ui-tests/src/test/kotlin/org/move/ui/UiTestBase.kt @@ -0,0 +1,42 @@ +package org.move.ui + +import com.intellij.remoterobot.RemoteRobot +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.io.TempDir +import org.move.ui.fixtures.closeProject +import org.move.ui.fixtures.removeLastRecentProject +import org.move.ui.utils.RemoteRobotExtension +import org.move.ui.utils.StepsLogger +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +@ExtendWith(RemoteRobotExtension::class) +open class UiTestBase { + init { + StepsLogger.init() + } + + protected fun getResourcesDir(): Path { + return Paths.get("").toAbsolutePath() + .resolve("src").resolve("test").resolve("resources") + } + + @TempDir + lateinit var tempFolder: File + + protected fun getExamplePackagesDir(): Path = getResourcesDir().resolve("example-packages") + protected fun copyExamplePackageToTempFolder(packageName: String): Path { + val tempPackagePath = tempFolder.toPath().resolve(packageName) + getExamplePackagesDir().resolve(packageName).toFile().copyRecursively(tempPackagePath.toFile()) + Thread.sleep(500) + return tempPackagePath + } + + @AfterEach + fun tearDown(robot: RemoteRobot) = with(robot) { + closeProject() + removeLastRecentProject() + } +} \ No newline at end of file diff --git a/ui-tests/src/test/resources/example-packages/aptos_package/Move.toml b/ui-tests/src/test/resources/example-packages/aptos_package/Move.toml new file mode 100644 index 000000000..822e35b0f --- /dev/null +++ b/ui-tests/src/test/resources/example-packages/aptos_package/Move.toml @@ -0,0 +1,15 @@ +[package] +name = "MyAptosPackage" +version = "1.0.0" +authors = [] + +[addresses] + +[dev-addresses] + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-core.git" +rev = "mainnet" +subdir = "aptos-move/framework/aptos-framework" + +[dev-dependencies] diff --git a/ui-tests/src/test/resources/example-packages/aptos_package/sources/.gitkeep b/ui-tests/src/test/resources/example-packages/aptos_package/sources/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ui-tests/src/test/resources/example-packages/empty_move_package/main.move b/ui-tests/src/test/resources/example-packages/empty_move_package/main.move new file mode 100644 index 000000000..77bb5c8ce --- /dev/null +++ b/ui-tests/src/test/resources/example-packages/empty_move_package/main.move @@ -0,0 +1,3 @@ +module 0x1::main { + fun main() {} +} diff --git a/ui-tests/src/test/resources/example-packages/sui_package/Move.toml b/ui-tests/src/test/resources/example-packages/sui_package/Move.toml new file mode 100644 index 000000000..ca3b2e85e --- /dev/null +++ b/ui-tests/src/test/resources/example-packages/sui_package/Move.toml @@ -0,0 +1,38 @@ +[package] +name = "MySuiPackage" + +# edition = "2024.alpha" # To use the Move 2024 edition, currently in alpha +# license = "" # e.g., "MIT", "GPL", "Apache 2.0" +# authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } + +# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. +# Revision can be a branch, a tag, and a commit hash. +# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } + +# For local dependencies use `local = path`. Path is relative to the package root +# Local = { local = "../path/to" } + +# To resolve a version conflict and force a specific version for dependency +# override use `override = true` +# Override = { local = "../conflicting/version", override = true } + +[addresses] +mysuipackage = "0x0" + +# Named addresses will be accessible in Move as `@name`. They're also exported: +# for example, `std = "0x1"` is exported by the Standard Library. +# alice = "0xA11CE" + +[dev-dependencies] +# The dev-dependencies section allows overriding dependencies for `--test` and +# `--dev` modes. You can introduce test-only dependencies here. +# Local = { local = "../path/to/dev-build" } + +[dev-addresses] +# The dev-addresses section allows overwriting named addresses for the `--test` +# and `--dev` modes. +# alice = "0xB0B" + diff --git a/ui-tests/src/test/resources/example-packages/sui_package/sources/.gitkeep b/ui-tests/src/test/resources/example-packages/sui_package/sources/.gitkeep new file mode 100644 index 000000000..e69de29bb