diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08062925..ddd805ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -193,7 +193,7 @@ jobs: # Run Qodana inspections - name: Qodana - Code Inspection - uses: JetBrains/qodana-action@v2024.1.9 + uses: JetBrains/qodana-action@v2024.2 with: cache-default-branch-only: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 392a5b39..64f829bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- Migrate formatter to AsyncDocumentFormattingService [[#391](https://github.com/koxudaxi/ruff-pycharm-plugin/pull/391)] - Remove projectRuffExecutablePath in config file [[#505](https://github.com/koxudaxi/ruff-pycharm-plugin/pull/505)] ## [0.0.39] - 2024-09-12 diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 4f3e54d4..c45ccee5 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -11,13 +11,13 @@ - - + + diff --git a/src/com/koxudaxi/ruff/Ruff.kt b/src/com/koxudaxi/ruff/Ruff.kt index 7c96e3c4..810cb443 100644 --- a/src/com/koxudaxi/ruff/Ruff.kt +++ b/src/com/koxudaxi/ruff/Ruff.kt @@ -273,15 +273,15 @@ fun runCommand( commandArgs.executable, commandArgs.project, commandArgs.stdin, *commandArgs.args.toTypedArray() ) - private fun getGeneralCommandLine(command: List, projectPath: String?): GeneralCommandLine = GeneralCommandLine(command).withWorkDirectory(projectPath).withCharset(Charsets.UTF_8) fun getGeneralCommandLine(executable: File, project: Project?, vararg args: String): GeneralCommandLine? { val projectPath = project?.basePath ?: return null if (!WslPath.isWslUncPath(executable.path)) { - return getGeneralCommandLine(listOf(executable.path) + args, projectPath) + return getGeneralCommandLine(listOf(executable.path) + args, projectPath) } + val windowsUncPath = WslPath.parseWindowsUncPath(executable.path) ?: return null val configArgIndex = args.indexOf(CONFIG_ARG) val injectedArgs = if (configArgIndex != -1 && configArgIndex < args.size - 1) { diff --git a/src/com/koxudaxi/ruff/RuffFixProcessor.kt b/src/com/koxudaxi/ruff/RuffAsyncFixFormatter.kt similarity index 51% rename from src/com/koxudaxi/ruff/RuffFixProcessor.kt rename to src/com/koxudaxi/ruff/RuffAsyncFixFormatter.kt index 9b9b5c07..c8718da9 100644 --- a/src/com/koxudaxi/ruff/RuffFixProcessor.kt +++ b/src/com/koxudaxi/ruff/RuffAsyncFixFormatter.kt @@ -3,9 +3,10 @@ package com.koxudaxi.ruff import com.intellij.openapi.project.Project -class RuffFixProcessor : RuffPostFormatProcessor() { +class RuffAsyncFixFormatter : RuffAsyncFormatterBase() { override fun isEnabled(project: Project): Boolean = RuffConfigService.getInstance(project).runRuffOnReformatCode - override fun process(sourceFile: SourceFile): String? = fix(sourceFile) + override fun getArgs(project: Project): List = project.FIX_ARGS + override fun getName(): String = "Ruff Fix Formatter" } \ No newline at end of file diff --git a/src/com/koxudaxi/ruff/RuffFormatProcessor.kt b/src/com/koxudaxi/ruff/RuffAsyncFormatFormatter.kt similarity index 55% rename from src/com/koxudaxi/ruff/RuffFormatProcessor.kt rename to src/com/koxudaxi/ruff/RuffAsyncFormatFormatter.kt index 2b926aa6..98720965 100644 --- a/src/com/koxudaxi/ruff/RuffFormatProcessor.kt +++ b/src/com/koxudaxi/ruff/RuffAsyncFormatFormatter.kt @@ -2,11 +2,13 @@ package com.koxudaxi.ruff import com.intellij.openapi.project.Project - -class RuffFormatProcessor : RuffPostFormatProcessor() { +class RuffAsyncFormatFormatter : RuffAsyncFormatterBase() { override fun isEnabled(project: Project): Boolean { val ruffConfigService = RuffConfigService.getInstance(project) - return ruffConfigService.runRuffOnReformatCode && ruffConfigService.useRuffFormat && RuffCacheService.hasFormatter(project) + return ruffConfigService.runRuffOnReformatCode && ruffConfigService.useRuffFormat } - override fun process(sourceFile: SourceFile): String? = format(sourceFile) + + override fun getArgs(project: Project): List = FORMAT_ARGS + override fun getName(): String = "Ruff Format Formatter" + } \ No newline at end of file diff --git a/src/com/koxudaxi/ruff/RuffAsyncFormatter.kt b/src/com/koxudaxi/ruff/RuffAsyncFormatter.kt new file mode 100644 index 00000000..7ce0e0c5 --- /dev/null +++ b/src/com/koxudaxi/ruff/RuffAsyncFormatter.kt @@ -0,0 +1,79 @@ +package com.koxudaxi.ruff + +import com.intellij.execution.ExecutionException +import com.intellij.execution.process.CapturingProcessAdapter +import com.intellij.execution.process.OSProcessHandler +import com.intellij.execution.process.ProcessEvent +import com.intellij.formatting.FormattingContext +import com.intellij.formatting.service.AsyncDocumentFormattingService +import com.intellij.formatting.service.AsyncFormattingRequest +import com.intellij.formatting.service.FormattingService +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import java.nio.charset.StandardCharsets +import java.util.* + + +abstract class RuffAsyncFormatterBase : AsyncDocumentFormattingService() { + abstract fun isEnabled(project: Project): Boolean + abstract fun getArgs(project: Project): List + private val FEATURES: MutableSet = EnumSet.noneOf( + FormattingService.Feature::class.java + ) + + override fun getFeatures(): MutableSet { + return FEATURES + } + + override fun canFormat(file: PsiFile): Boolean { + return isEnabled(file.project) && file.isApplicableTo + } + + override fun createFormattingTask(request: AsyncFormattingRequest): FormattingTask? { + val formattingContext: FormattingContext = request.context + val ioFile = request.ioFile ?: return null + val sourceFile = formattingContext.containingFile.sourceFile + val commandArgs = generateCommandArgs(sourceFile, getArgs(formattingContext.project)) ?: return null + try { + val commandLine = + getGeneralCommandLine(commandArgs.executable, commandArgs.project, *commandArgs.args.toTypedArray()) + ?: return null + val handler = OSProcessHandler(commandLine.withCharset(StandardCharsets.UTF_8)) + with(handler) { + processInput.write(ioFile.readText().toByteArray()) + processInput.close() + } + return object : FormattingTask { + override fun run() { + handler.addProcessListener(object : CapturingProcessAdapter() { + override fun processTerminated(event: ProcessEvent) { + val exitCode = event.exitCode + if (exitCode == 0) { + request.onTextReady(output.stdout) + } else { + request.onError("Ruff Error", output.stderr) + } + } + }) + handler.startNotify() + } + + override fun cancel(): Boolean { + handler.destroyProcess() + return true + } + + override fun isRunUnderProgress(): Boolean { + return true + } + } + } catch (e: ExecutionException) { + e.message?.let { request.onError("Ruff Error", it) } + return null + } + } + + override fun getNotificationGroupId(): String { + return "Ruff" + } +} \ No newline at end of file diff --git a/src/com/koxudaxi/ruff/RuffPostFormatProcessor.kt b/src/com/koxudaxi/ruff/RuffPostFormatProcessor.kt deleted file mode 100644 index 05c6bdb2..00000000 --- a/src/com/koxudaxi/ruff/RuffPostFormatProcessor.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.koxudaxi.ruff - -import com.intellij.openapi.editor.Document -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.codeStyle.CodeStyleSettings -import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor -import com.jetbrains.python.psi.PyUtil - - -abstract class RuffPostFormatProcessor : PostFormatProcessor { - override fun processElement(source: PsiElement, settings: CodeStyleSettings): PsiElement = source - override fun processText(source: PsiFile, rangeToReformat: TextRange, settings: CodeStyleSettings): TextRange { - if (!isEnabled(source.project)) return rangeToReformat - if (!source.isApplicableTo) return rangeToReformat - - val sourceFile = source.getSourceFile(rangeToReformat, true) - val text = sourceFile.text ?: return rangeToReformat - val formatted = executeOnPooledThread(null) { - process(sourceFile) - } ?: return rangeToReformat - if (text == formatted) return rangeToReformat - val sourceDiffRange = diffRange(text, formatted) ?: return rangeToReformat - - return PyUtil.updateDocumentUnblockedAndCommitted( - source - ) { document: Document -> - // Verify that the document has not been modified since the reformatting started - if (!sourceFile.hasSameContentAsDocument(document)) return@updateDocumentUnblockedAndCommitted rangeToReformat - - // document.replaceString(startOffset, endOffset, formattedPart) does not keep the line break point markers and caret position - document.setText(formatted) - - val endOffset = rangeToReformat.startOffset + sourceDiffRange.length - when { - endOffset <= rangeToReformat.endOffset -> rangeToReformat - else -> TextRange.create(rangeToReformat.startOffset, endOffset) - } - } ?: rangeToReformat - } - - abstract fun isEnabled(project: Project): Boolean - abstract fun process(sourceFile: SourceFile): String? - - internal fun diffRange(source: String, target: String): TextRange? { - if (source == target) return null - if (source.isEmpty() || target.isEmpty()) return TextRange(0, source.length) - - val start = source.zip(target).takeWhile { it.first == it.second }.count() - - var endSource = source.lastIndex - var endTarget = target.lastIndex - while (endSource >= start && endTarget >= start && source[endSource] == target[endTarget]) { - endSource-- - endTarget-- - } - return TextRange(start, endSource + 1) - } -} \ No newline at end of file diff --git a/testSrc/com/koxudaxi/ruff/RuffPostFormatProcessorTest.kt b/testSrc/com/koxudaxi/ruff/RuffPostFormatProcessorTest.kt deleted file mode 100644 index 8380cbf4..00000000 --- a/testSrc/com/koxudaxi/ruff/RuffPostFormatProcessorTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.koxudaxi.ruff - -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.TextRange -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull - - -class RuffPostFormatProcessorTest { - class RuffPostFormatProcessorBase : RuffPostFormatProcessor() { - override fun isEnabled(project: Project): Boolean { - TODO("Not yet implemented") - } - - override fun process(sourceFile: SourceFile): String? { - TODO("Not yet implemented") - } - } - - private val postFormatProcessor = RuffPostFormatProcessorBase() - private fun diffRange(source: String, target: String): TextRange? { - return postFormatProcessor.diffRange(source, target) - } - - private fun doTestDiffRange(source: String, target: String, expectTextRange: TextRange, expectInserted: String) { - val sourceDiffRange = diffRange(source, target) - assertNotNull(sourceDiffRange) - assertEquals(expectTextRange, sourceDiffRange) - val targetDiffRange = diffRange(target, source) - assertNotNull(targetDiffRange) - val inserted = target.substring(targetDiffRange.startOffset, targetDiffRange.endOffset) - assertEquals(expectInserted, targetDiffRange.substring(target)) - assertEquals(target, sourceDiffRange.replace(source, inserted)) - } - - @Test - fun testDiffRange() { - assertNull(diffRange("hello", "hello")) // no difference - doTestDiffRange("hello", "", TextRange(0, 5), "") // complete difference - doTestDiffRange("hello", "hell", TextRange(4, 5), "") // difference at the end - doTestDiffRange("hell", "hello", TextRange(4, 4), "o") // difference at the end - doTestDiffRange("hello", "jello", TextRange(0, 1), "j") // difference at the start - doTestDiffRange("hello world", "hello world!", TextRange(11, 11), "!") // difference at the end - doTestDiffRange("hello world!", "hello world", TextRange(11, 12), "") // difference at the end - doTestDiffRange("hello world", "Hello world", TextRange(0, 1), "H") // difference at the start - doTestDiffRange("hello world", "hEllo world", TextRange(1, 2), "E") // difference in the middle - doTestDiffRange("import a\nimport b\nimport c\n", "import a\nimport c\n", TextRange(16, 25), "") - doTestDiffRange( - "import a\nimport c\n", - "import a\nimport b\nimport c\n", - TextRange(16, 16), - "b\nimport " - ) // difference in the middle - } -} \ No newline at end of file