diff --git a/src/org/elixir_lang/formatter/MixFmtExternalFormatProcessor.java b/src/org/elixir_lang/formatter/MixFmtExternalFormatProcessor.java deleted file mode 100644 index 06edf10ed..000000000 --- a/src/org/elixir_lang/formatter/MixFmtExternalFormatProcessor.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.elixir_lang.formatter; - -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.command.CommandProcessor; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiFile; -import com.intellij.psi.codeStyle.ExternalFormatProcessor; -import dev.monogon.cue.cli.CueCommandService; -import org.elixir_lang.psi.ElixirFile; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.concurrent.TimeUnit; - -@SuppressWarnings("UnstableApiUsage") -public class MixFmtExternalFormatProcessor implements ExternalFormatProcessor { - private static final Logger LOG = Logger.getInstance(MixFmtExternalFormatProcessor.class); - - @Override - public boolean activeForFile(@NotNull PsiFile source) { - return source instanceof ElixirFile; - } - - @Override - public @NonNls - @NotNull String getId() { - return "elixir_lang.mixFmtExternalFormatProcessor"; - } - - // compatibility with 2021.1+ - // @Override - public @Nullable TextRange format(@NotNull PsiFile file, - @NotNull TextRange range, - boolean canChangeWhiteSpacesOnly, - boolean keepLineBreaks, - boolean enableBulkUpdate) { - return format(file, range, canChangeWhiteSpacesOnly, keepLineBreaks); - } - - // compatibility with 2020.3 - // @Override - public @Nullable TextRange format(@NotNull PsiFile file, - @NotNull TextRange range, - boolean canChangeWhiteSpacesOnly, - boolean keepLineBreaks) { - if (!file.isValid()) { - return null; - } - - // mix fmt doesn't support to format parts of a file - if (!range.equals(file.getTextRange())) { - return null; - } - - var document = file.getViewProvider().getDocument(); - if (document == null) { - return null; - } - - var app = ApplicationManager.getApplication(); - app.executeOnPooledThread(() -> { - try { - var newContent = CueCommandService.getInstance().format(document.getText(), 5, TimeUnit.SECONDS); - if (newContent != null) { - app.invokeLater(() -> { - if (!file.isValid()) { - return; - } - - var processor = CommandProcessor.getInstance(); - processor.runUndoTransparentAction(() -> app.runWriteAction(() -> document.setText(newContent))); - }); - } - } - catch (Exception e) { - LOG.debug("error executing mix fmt", e); - } - }); - return file.getTextRange(); - } - - @Override - public @Nullable String indent(@NotNull PsiFile source, int lineStartOffset) { - return null; - } -} diff --git a/src/org/elixir_lang/formatter/MixFmtExternalFormatProcessor.kt b/src/org/elixir_lang/formatter/MixFmtExternalFormatProcessor.kt new file mode 100644 index 000000000..f88a8bbad --- /dev/null +++ b/src/org/elixir_lang/formatter/MixFmtExternalFormatProcessor.kt @@ -0,0 +1,110 @@ +package org.elixir_lang.formatter + +import com.intellij.execution.process.CapturingProcessHandler +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiFile +import com.intellij.psi.codeStyle.ExternalFormatProcessor +import org.elixir_lang.Mix +import org.elixir_lang.psi.ElixirFile +import org.elixir_lang.sdk.elixir.Type.Companion.mostSpecificSdk +import java.util.concurrent.TimeUnit + +@Suppress("UnstableApiUsage") +@SuppressWarnings("UnstableApiUsage") +class MixFmtExternalFormatProcessor : ExternalFormatProcessor { + override fun activeForFile(source: PsiFile): Boolean { + return source is ElixirFile + } + + override fun getId(): String = "elixir_lang.mixFmtExternalFormatProcessor" + + override fun format( + source: PsiFile, + range: TextRange, + canChangeWhiteSpacesOnly: Boolean, + keepLineBreaks: Boolean, + enableBulkUpdate: Boolean, + cursorOffset: Int + ): TextRange? = + // mix fmt doesn't support to format parts of a file + if (source.isValid && range == source.textRange) { + source.viewProvider.document?.let { document -> + val application = ApplicationManager.getApplication() + + application.executeOnPooledThread { + workingDirectory(source)?.let { workingDirectory -> + mostSpecificSdk(source)?.let { sdk -> + format(workingDirectory, sdk, document.text)?.let { formattedText -> + application.invokeLater { + if (source.isValid) { + CommandProcessor.getInstance().runUndoTransparentAction { + application.runWriteAction { + document.setText(formattedText) + } + } + } + } + } + } + } + } + + source.textRange + } + } else { + null + } + + override fun indent(source: PsiFile, lineStartOffset: Int): String? = null + + companion object { + private val LOGGER = Logger.getInstance(MixFmtExternalFormatProcessor::class.java) + + private fun workingDirectory(psiFile: PsiFile): String? = + psiFile + .virtualFile + ?.let { virtualFile -> + ProjectRootManager.getInstance(psiFile.project).fileIndex.getContentRootForFile(virtualFile) + } + ?.takeIf { contentRoot -> + contentRoot.findChild("mix.exs") != null + } + ?.path + + private fun format(workingDirectory: String, sdk: Sdk, unformattedText: String): String? { + val commandline = Mix.commandLine(emptyMap(), workingDirectory, sdk) + commandline.addParameter("format") + // `-` turns on stdin/stdout for text to format + commandline.addParameter("-") + val processHandler = CapturingProcessHandler(commandline) + processHandler.processInput.use { stdin -> + stdin.write(unformattedText.toByteArray()) + stdin.flush() + } + + val indicator = ProgressManager.getGlobalProgressIndicator() + val timeout = TimeUnit.SECONDS.toMillis(5).toInt() + val output = if (indicator != null) { + processHandler.runProcessWithProgressIndicator( + indicator, + timeout, + true + ) + } else { + processHandler.runProcess(timeout, true) + } + + return if (output.checkSuccess(LOGGER)) { + output.stdout + } else { + null + } + } + } +}