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" /> +