diff --git a/src/main/kotlin/org/move/cli/externalFormatter/MovefmtConfigurable.kt b/src/main/kotlin/org/move/cli/externalFormatter/MovefmtConfigurable.kt
new file mode 100644
index 00000000..cd3b4c96
--- /dev/null
+++ b/src/main/kotlin/org/move/cli/externalFormatter/MovefmtConfigurable.kt
@@ -0,0 +1,93 @@
+package org.move.cli.externalFormatter
+
+import com.intellij.execution.configuration.EnvironmentVariablesComponent
+import com.intellij.execution.configuration.EnvironmentVariablesData
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
+import com.intellij.openapi.options.BoundConfigurable
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.DialogPanel
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.util.io.toNioPathOrNull
+import com.intellij.ui.RawCommandLineEditor
+import com.intellij.ui.dsl.builder.AlignX
+import com.intellij.ui.dsl.builder.bindSelected
+import com.intellij.ui.dsl.builder.panel
+import com.intellij.ui.dsl.builder.toMutableProperty
+import org.move.cli.settings.VersionLabel
+import org.move.openapiext.pathField
+
+class MovefmtConfigurable(val project: Project): BoundConfigurable("Movefmt") {
+ private val innerDisposable =
+ Disposer.newCheckedDisposable("Internal checked disposable for MovefmtConfigurable")
+
+ private val movefmtPathField =
+ pathField(
+ FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor(),
+ innerDisposable,
+ "Movefmt location",
+ onTextChanged = { it ->
+ val path = it.toNioPathOrNull() ?: return@pathField
+ versionLabel.update(path)
+ })
+ private val versionLabel = VersionLabel(
+ innerDisposable,
+ envs = EnvironmentVariablesData.create(mapOf("MOVEFMT_LOG" to "error"), true)
+ )
+
+ private val additionalArguments: RawCommandLineEditor = RawCommandLineEditor()
+ private val environmentVariables: EnvironmentVariablesComponent = EnvironmentVariablesComponent()
+
+
+ override fun createPanel(): DialogPanel {
+ this.disposable?.let {
+ Disposer.register(it, innerDisposable)
+ }
+ return panel {
+ val settings = project.movefmtSettings
+ val state = settings.state.copy()
+
+ row("Movefmt:") {
+ cell(movefmtPathField)
+ .align(AlignX.FILL).resizableColumn()
+ .bind(
+ componentGet = { it.text },
+ componentSet = { component, value -> component.text = value },
+ prop = state::movefmtPath.toMutableProperty()
+ )
+ }
+ row("--version :") { cell(versionLabel) }
+ separator()
+ row("Additional arguments:") {
+ cell(additionalArguments)
+ .align(AlignX.FILL)
+ .comment("Additional arguments to pass to movefmt command")
+ .bind(
+ componentGet = { it.text },
+ componentSet = { component, value -> component.text = value },
+ prop = state::additionalArguments.toMutableProperty()
+ )
+ }
+ row(environmentVariables.label) {
+ cell(environmentVariables).align(AlignX.FILL)
+ .bind(
+ componentGet = { it.envs },
+ componentSet = { component, value -> component.envs = value },
+ prop = state::envs.toMutableProperty()
+ )
+ }
+
+ row { checkBox("Use movefmt instead of the built-in formatter").bindSelected(state::useMovefmt) }
+// row { checkBox("Run movefmt on Save").bindSelected(state::runRustfmtOnSave) }
+
+ onApply {
+ settings.modify {
+ it.movefmtPath = state.movefmtPath
+ it.additionalArguments = state.additionalArguments
+ it.envs = state.envs
+ it.useMovefmt = state.useMovefmt
+// it.runRustfmtOnSave = state.runRustfmtOnSave
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/org/move/cli/externalFormatter/MovefmtFormattingService.kt b/src/main/kotlin/org/move/cli/externalFormatter/MovefmtFormattingService.kt
new file mode 100644
index 00000000..2bec0438
--- /dev/null
+++ b/src/main/kotlin/org/move/cli/externalFormatter/MovefmtFormattingService.kt
@@ -0,0 +1,125 @@
+package org.move.cli.externalFormatter
+
+import com.intellij.codeInsight.actions.ReformatCodeProcessor
+import com.intellij.execution.configuration.EnvironmentVariablesData
+import com.intellij.execution.process.CapturingProcessAdapter
+import com.intellij.execution.process.ProcessEvent
+import com.intellij.formatting.service.AsyncDocumentFormattingService
+import com.intellij.formatting.service.AsyncFormattingRequest
+import com.intellij.formatting.service.FormattingService
+import com.intellij.notification.NotificationType.ERROR
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.command.CommandProcessor
+import com.intellij.openapi.progress.util.ProgressIndicatorBase
+import com.intellij.openapi.project.DumbAwareAction
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.util.io.toNioPathOrNull
+import com.intellij.psi.PsiFile
+import com.intellij.psi.formatter.FormatterUtil
+import org.move.cli.externalFormatter.MovefmtFormattingService.Companion.FormattingReason.*
+import org.move.cli.tools.Movefmt
+import org.move.ide.notifications.showBalloon
+import org.move.lang.MoveFile
+import org.move.openapiext.rootPath
+import org.move.openapiext.showSettingsDialog
+import org.move.stdext.blankToNull
+import org.move.stdext.enumSetOf
+import org.move.stdext.unwrapOrThrow
+
+class MovefmtFormattingService: AsyncDocumentFormattingService() {
+ // only whole file formatting is supported
+ override fun getFeatures(): Set = enumSetOf()
+
+ override fun canFormat(file: PsiFile): Boolean =
+ file is MoveFile && file.project.movefmtSettings.useMovefmt && getFormattingReason() == ReformatCode
+
+ override fun createFormattingTask(request: AsyncFormattingRequest): FormattingTask? {
+ val context = request.context
+ val project = context.project
+ val settings = project.movefmtSettings
+
+ val disposable = Disposer.newDisposable()
+ val movefmt = project.getMovefmt(disposable)
+ if (movefmt == null) {
+ project.showBalloon(MOVEFMT_ERROR,
+ "movefmt executable configured incorrectly",
+ ERROR,
+ object : DumbAwareAction("Edit movefmt settings") {
+ override fun actionPerformed(e: AnActionEvent) {
+ e.project?.showSettingsDialog()
+ }
+ }
+ )
+ return null
+ }
+
+ val projectDirectory = project.rootPath ?: return null
+ val fileOnDisk = request.ioFile ?: return null
+
+ return object: FormattingTask {
+ private val indicator: ProgressIndicatorBase = ProgressIndicatorBase()
+
+ override fun run() {
+ val arguments = settings.additionalArguments.blankToNull()?.split(" ").orEmpty()
+ val envs = EnvironmentVariablesData.create(settings.envs, true)
+ movefmt.reformatFile(
+ fileOnDisk,
+ additionalArguments = arguments,
+ workingDirectory = projectDirectory,
+ envs,
+ runner = {
+ addProcessListener(object: CapturingProcessAdapter() {
+ override fun processTerminated(event: ProcessEvent) {
+ val exitCode = event.exitCode
+ if (exitCode == 0) {
+ val filteredStdout = filterBuggyLines(output.stdout)
+ request.onTextReady(filteredStdout)
+ } else {
+ request.onError("Movefmt", output.stderr)
+ }
+ }
+ })
+ runProcessWithProgressIndicator(indicator)
+ }
+ )
+ .unwrapOrThrow()
+ }
+
+ override fun cancel(): Boolean {
+ indicator.cancel()
+ disposable.dispose()
+ return true
+ }
+
+ override fun isRunUnderProgress(): Boolean = true
+ }
+ }
+
+ private fun filterBuggyLines(stdout: String): String {
+ return stdout.lines()
+ .takeWhile { !it.contains("files successfully formatted") }
+ .joinToString("\n")
+ }
+
+ override fun getNotificationGroupId(): String = "Move Language"
+
+ override fun getName(): String = "movefmt"
+
+ companion object {
+ private const val MOVEFMT_ERROR = "movefmt error"
+
+ private enum class FormattingReason {
+ ReformatCode,
+ ReformatCodeBeforeCommit,
+ Implicit
+ }
+
+ private fun getFormattingReason(): FormattingReason =
+ when (CommandProcessor.getInstance().currentCommandName) {
+ ReformatCodeProcessor.getCommandName() -> ReformatCode
+ FormatterUtil.getReformatBeforeCommitCommandName() -> ReformatCodeBeforeCommit
+ else -> Implicit
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/org/move/cli/externalFormatter/MovefmtSettingsService.kt b/src/main/kotlin/org/move/cli/externalFormatter/MovefmtSettingsService.kt
new file mode 100644
index 00000000..1f3ad7b6
--- /dev/null
+++ b/src/main/kotlin/org/move/cli/externalFormatter/MovefmtSettingsService.kt
@@ -0,0 +1,68 @@
+package org.move.cli.externalFormatter
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.components.*
+import com.intellij.openapi.components.Service.Level.PROJECT
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.io.toNioPathOrNull
+import com.intellij.util.text.nullize
+import org.move.cli.externalFormatter.MoveFmtSettingsService.MoveFmtSettings
+import org.move.cli.runConfigurations.aptos.Aptos
+import org.move.cli.settings.MvProjectSettingsServiceBase
+import org.move.cli.settings.aptosCliPath
+import org.move.cli.settings.isValidExecutable
+import org.move.cli.tools.Movefmt
+import org.move.openapiext.RootPluginDisposable
+
+private const val SERVICE_NAME: String = "org.move.MoveFmtSettingsService"
+
+@State(
+ name = SERVICE_NAME,
+ storages = [Storage(StoragePathMacros.WORKSPACE_FILE)],
+)
+@Service(PROJECT)
+class MoveFmtSettingsService(
+ project: Project,
+): MvProjectSettingsServiceBase(project, MoveFmtSettings()) {
+
+ val useMovefmt: Boolean get() = state.useMovefmt
+// val runMovefmtOnSave: Boolean get() = state.runMovefmtOnSave
+
+ val movefmtPath: String? get() = state.movefmtPath.nullize()
+ val additionalArguments: String get() = state.additionalArguments
+ val envs: Map get() = state.envs
+
+ class MoveFmtSettings: MvProjectSettingsBase() {
+ var useMovefmt by property(false)
+// var runMovefmtOnSave by property(false)
+
+ var movefmtPath by property("") { it.isEmpty() }
+ var additionalArguments by property("") { it.isEmpty() }
+ var envs by map()
+
+ override fun copy(): MoveFmtSettings {
+ val state = MoveFmtSettings()
+ state.copyFrom(this)
+ return state
+ }
+ }
+
+ override fun createSettingsChangedEvent(
+ oldEvent: MoveFmtSettings,
+ newEvent: MoveFmtSettings
+ ): SettingsChangedEvent = SettingsChangedEvent(oldEvent, newEvent)
+
+ class SettingsChangedEvent(
+ oldState: MoveFmtSettings,
+ newState: MoveFmtSettings
+ ): SettingsChangedEventBase(oldState, newState)
+}
+
+val Project.movefmtSettings: MoveFmtSettingsService get() = service()
+
+fun Project.getMovefmt(disposable: Disposable): Movefmt? {
+ val settings = this.movefmtSettings
+ val movefmtPath = settings.movefmtPath?.toNioPathOrNull()?.takeIf { it.isValidExecutable() } ?: return null
+ return Movefmt(movefmtPath, disposable)
+}
+
diff --git a/src/main/kotlin/org/move/cli/settings/VersionLabel.kt b/src/main/kotlin/org/move/cli/settings/VersionLabel.kt
index ec705dc1..4b3834d3 100644
--- a/src/main/kotlin/org/move/cli/settings/VersionLabel.kt
+++ b/src/main/kotlin/org/move/cli/settings/VersionLabel.kt
@@ -1,10 +1,10 @@
package org.move.cli.settings
-import com.intellij.openapi.Disposable
+import com.intellij.execution.configuration.EnvironmentVariablesData
import com.intellij.openapi.util.CheckedDisposable
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBLabel
-import org.move.cli.runConfigurations.AptosCommandLine
+import org.move.cli.tools.MvCommandLine
import org.move.openapiext.UiDebouncer
import org.move.openapiext.checkIsBackgroundThread
import org.move.openapiext.common.isUnitTestMode
@@ -32,13 +32,14 @@ open class TextOrErrorLabel(icon: Icon?): JBLabel(icon) {
class VersionLabel(
parentDisposable: CheckedDisposable,
+ private val envs: EnvironmentVariablesData = EnvironmentVariablesData.DEFAULT,
private val versionUpdateListener: (() -> Unit)? = null
):
TextOrErrorLabel(null) {
private val versionUpdateDebouncer = UiDebouncer(parentDisposable)
- fun updateAndNotifyListeners(execPath: Path?) {
+ fun update(execPath: Path?) {
versionUpdateDebouncer.update(
onPooledThread = {
if (!isUnitTestMode) {
@@ -48,8 +49,11 @@ class VersionLabel(
return@update null
}
- val commandLineArgs =
- AptosCommandLine(null, listOf("--version"), workingDirectory = null)
+ val commandLineArgs = MvCommandLine(
+ listOf("--version"),
+ workingDirectory = null,
+ environmentVariables = envs
+ )
commandLineArgs
.toGeneralCommandLine(execPath)
.execute()
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 6fe954ca..eae31fe8 100644
--- a/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt
+++ b/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt
@@ -106,7 +106,7 @@ class ChooseAptosCliPanel(versionUpdateListener: (() -> Unit)?): Disposable {
onTextChanged = { _ ->
updateVersion()
})
- private val versionLabel = VersionLabel(innerDisposable, versionUpdateListener)
+ private val versionLabel = VersionLabel(innerDisposable, versionUpdateListener = versionUpdateListener)
private val bundledRadioButton = JBRadioButton("Bundled")
private val localRadioButton = JBRadioButton("Local")
@@ -194,7 +194,7 @@ class ChooseAptosCliPanel(versionUpdateListener: (() -> Unit)?): Disposable {
private fun updateVersion() {
val aptosPath =
if (isBundledSelected) AptosExecType.bundledAptosCLIPath else localPathField.text.toNioPathOrNull()
- versionLabel.updateAndNotifyListeners(aptosPath)
+ versionLabel.update(aptosPath)
}
fun updateAptosSdks(sdkPath: String) {
diff --git a/src/main/kotlin/org/move/cli/tools/Movefmt.kt b/src/main/kotlin/org/move/cli/tools/Movefmt.kt
new file mode 100644
index 00000000..058abbad
--- /dev/null
+++ b/src/main/kotlin/org/move/cli/tools/Movefmt.kt
@@ -0,0 +1,54 @@
+package org.move.cli.tools
+
+import com.intellij.execution.configuration.EnvironmentVariablesData
+import com.intellij.execution.process.CapturingProcessHandler
+import com.intellij.execution.process.ProcessOutput
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.util.Disposer
+import com.intellij.util.containers.addAllIfNotNull
+import org.move.openapiext.RsProcessResult
+import org.move.openapiext.execute
+import org.move.openapiext.runProcessWithGlobalProgress
+import java.io.File
+import java.nio.file.Path
+
+
+class Movefmt(val cliLocation: Path, val parentDisposable: Disposable): Disposable.Default {
+
+ // cannot make Movefmt CheckedDisposable, need to use another one
+ private val innerDisposable = Disposer.newCheckedDisposable("Movefmt disposable")
+
+ init {
+ Disposer.register(this, innerDisposable)
+ Disposer.register(parentDisposable, this)
+ }
+
+ fun reformatFile(
+ file: File,
+ additionalArguments: List,
+ workingDirectory: Path,
+ envs: EnvironmentVariablesData,
+ runner: CapturingProcessHandler.() -> ProcessOutput = { runProcessWithGlobalProgress() }
+ ): RsProcessResult {
+ val commandLine = MvCommandLine(
+ buildList {
+ add("-q")
+ addAllIfNotNull("--emit", "stdout")
+ if (additionalArguments.isNotEmpty()) {
+ addAll(additionalArguments)
+ }
+ add(file.absolutePath)
+ },
+ workingDirectory = workingDirectory,
+ environmentVariables = envs.with(mapOf("MOVEFMT_LOG" to "error")),
+ )
+ return commandLine
+ .toGeneralCommandLine(this.cliLocation)
+ // needs to skip stderr here
+ .withRedirectErrorStream(false)
+ .execute(
+ innerDisposable,
+ runner,
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/org/move/ide/structureView/MvStructureViewTreeElement.kt b/src/main/kotlin/org/move/ide/structureView/MvStructureViewTreeElement.kt
index 250ba5db..dd990797 100644
--- a/src/main/kotlin/org/move/ide/structureView/MvStructureViewTreeElement.kt
+++ b/src/main/kotlin/org/move/ide/structureView/MvStructureViewTreeElement.kt
@@ -1,8 +1,6 @@
package org.move.ide.structureView
-import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.structureView.StructureViewTreeElement
-import com.intellij.ide.util.treeView.TreeAnchorizer
import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.ui.Queryable
@@ -15,13 +13,13 @@ import org.move.lang.core.psi.*
import org.move.lang.core.psi.ext.*
import org.move.openapiext.common.isUnitTestMode
-class MvStructureViewTreeElement(element: NavigatablePsiElement): StructureViewTreeElement,
+class MvStructureViewTreeElement(val psi: NavigatablePsiElement): StructureViewTreeElement,
Queryable {
- val psiAnchor = TreeAnchorizer.getService().createAnchor(element)
- val psi: NavigatablePsiElement?
- get() =
- TreeAnchorizer.getService().retrieveElement(psiAnchor) as? NavigatablePsiElement
+// val psiAnchor = TreeAnchorizer.getService().createAnchor(element)
+// val psi: NavigatablePsiElement?
+// get() =
+// TreeAnchorizer.getService().retrieveElement(psiAnchor) as? NavigatablePsiElement
val isPublicItem: Boolean =
when (val psi = psi) {
@@ -34,16 +32,12 @@ class MvStructureViewTreeElement(element: NavigatablePsiElement): StructureViewT
val isTestOnlyItem: Boolean get() = (psi as? MvDocAndAttributeOwner)?.hasTestOnlyAttr == true
- override fun navigate(requestFocus: Boolean) {
- psi?.navigate(requestFocus)
- }
-
- override fun canNavigate(): Boolean = psi?.canNavigate() == true
- override fun canNavigateToSource(): Boolean = psi?.canNavigateToSource() == true
- override fun getValue(): PsiElement? = psi
+ override fun navigate(requestFocus: Boolean) = psi.navigate(requestFocus)
+ override fun canNavigate(): Boolean = psi.canNavigate()
+ override fun canNavigateToSource(): Boolean = psi.canNavigateToSource()
+ override fun getValue(): PsiElement = psi
- override fun getPresentation(): ItemPresentation =
- psi?.let(::getPresentationForStructure) ?: PresentationData("", null, null, null)
+ override fun getPresentation(): ItemPresentation = psi.let(::getPresentationForStructure)
override fun getChildren(): Array =
childElements.map2Array { MvStructureViewTreeElement(it) }
diff --git a/src/main/kotlin/org/move/lang/MoveFile.kt b/src/main/kotlin/org/move/lang/MoveFile.kt
index f39658bf..a7106728 100644
--- a/src/main/kotlin/org/move/lang/MoveFile.kt
+++ b/src/main/kotlin/org/move/lang/MoveFile.kt
@@ -77,6 +77,7 @@ class MoveFile(fileViewProvider: FileViewProvider) : MoveFileBase(fileViewProvid
fun moduleSpecs(): List = this.childrenOfType()
}
+val VirtualFile.isNotMoveFile: Boolean get() = !isMoveFile
val VirtualFile.isMoveFile: Boolean get() = fileType == MoveFileType
val VirtualFile.isMoveTomlManifestFile: Boolean get() = name == "Move.toml"
diff --git a/src/main/kotlin/org/move/openapiext/utils.kt b/src/main/kotlin/org/move/openapiext/utils.kt
index 85af5abf..ed5b0add 100644
--- a/src/main/kotlin/org/move/openapiext/utils.kt
+++ b/src/main/kotlin/org/move/openapiext/utils.kt
@@ -31,6 +31,7 @@ import com.intellij.openapi.roots.SyntheticLibrary
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.JDOMUtil
import com.intellij.openapi.util.NlsContexts
+import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.*
@@ -69,6 +70,12 @@ fun VirtualFile.toPsiFile(project: Project): PsiFile? =
fun VirtualFile.toPsiDirectory(project: Project): PsiDirectory? =
PsiManager.getInstance(project).findDirectory(this)
+val Document.virtualFile: VirtualFile?
+ get() = FileDocumentManager.getInstance().getFile(this)
+
+val VirtualFile.document: Document?
+ get() = FileDocumentManager.getInstance().getDocument(this)
+
val PsiFile.document: Document?
get() = PsiDocumentManager.getInstance(project).getDocument(this)
@@ -245,3 +252,5 @@ inline fun testAssert(action: () -> Boolean, lazyMessage: () -> Any) {
throw AssertionError(message)
}
}
+
+val String.escaped: String get() = StringUtil.escapeXmlEntities(this)
\ No newline at end of file
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 90394725..daa732cb 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -105,6 +105,8 @@
+
+
@@ -340,6 +342,10 @@
parentId="language.move"
id="language.move.compiler.check"
displayName="External Linters" />
+