From b5a25013423d3c6effb9011c4c39024bc5b969ac Mon Sep 17 00:00:00 2001 From: Alessandro Arcangeli Date: Sat, 22 Feb 2025 17:17:05 +0100 Subject: [PATCH] Use built in clang-format + fixes --- .gitattributes | 2 + .idea/gradle.xml | 3 +- .../ideaclangformat/ClangFormatConfig.kt | 8 ++ .../configurable/AppConfigurable.kt | 121 ++++++++++++++---- .../services/ClangFormatService.kt | 17 ++- .../services/ClangFormatServiceImpl.kt | 73 ++++++++++- .../services/ClangFormatStyleService.kt | 2 +- .../services/ClangFormatStyleServiceImpl.kt | 31 +++-- .../utils/ClangFormatCommons.kt | 19 +++ .../clang-format-linux-aarch64/clang-format | 3 + .../clang-format-linux-armv7a/clang-format | 3 + .../clang-format-macos-arm64/clang-format | 3 + .../clang-format-macos-x64/clang-format | 3 + src/main/resources/clang-format-tag.txt | 1 + .../clang-format-win/clang-format.exe | 3 + 15 files changed, 248 insertions(+), 44 deletions(-) create mode 100644 .gitattributes create mode 100644 src/main/resources/clang-format-linux-aarch64/clang-format create mode 100644 src/main/resources/clang-format-linux-armv7a/clang-format create mode 100644 src/main/resources/clang-format-macos-arm64/clang-format create mode 100644 src/main/resources/clang-format-macos-x64/clang-format create mode 100644 src/main/resources/clang-format-tag.txt create mode 100644 src/main/resources/clang-format-win/clang-format.exe diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a164660 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.exe filter=lfs diff=lfs merge=lfs -text +clang-format filter=lfs diff=lfs merge=lfs -text diff --git a/.idea/gradle.xml b/.idea/gradle.xml index e93b4c4..2f68d40 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -5,6 +5,7 @@ - + \ No newline at end of file diff --git a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/ClangFormatConfig.kt b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/ClangFormatConfig.kt index dffa3a0..7c163f6 100644 --- a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/ClangFormatConfig.kt +++ b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/ClangFormatConfig.kt @@ -14,6 +14,8 @@ class ClangFormatConfig : SimplePersistentStateComponent private val settings = service().state - private lateinit var version: Cell + private lateinit var version: JTextArea private lateinit var pathField: Cell override fun getId(): String = "aarcangeli.ideaclangformat.appconfig" override fun getDisplayName(): String = "Clang-Format Tools" + private lateinit var builtinRadio: JRadioButton + private lateinit var detectRadio: JRadioButton + private lateinit var customRadio: JRadioButton + override fun createPanel(): DialogPanel { return panel { group("Options") { row { - combobox = checkBox("Enable Clang-Format support") + combobox = checkBox("Enable reformatting with Clang-Format") .comment("When disabled, Clang-Format will not be used") .bindSelected(settings::enabled) .onApply { @@ -53,44 +65,105 @@ class AppConfigurable : DslConfigurableBase(), SearchableConfigurable, NoScroll } group("Location") { - row("Auto-detected:") { - val detectFromPath = service().detectFromPath() - textField() - .align(AlignX.FILL) - .component.apply { - text = detectFromPath ?: "Not found" - isEditable = false - } - } - row("Custom Path:") { - pathField = textFieldWithBrowseButton("Clang-Format Path") - .align(AlignX.FILL) - .bindText({ settings.customPath ?: "" }, { settings.customPath = it }) + buttonsGroup { + row { + val builtin = service().getBuiltinPath() + builtinRadio = radioButton("Built-in (${builtin?.version ?: "not available"})") + .bindSelected({ settings.location == ClangFormatToUse.BUILTIN }, { settings.location = ClangFormatToUse.BUILTIN }) + .gap(RightGap.SMALL) + .onChanged { resetVersion() } + .enabled(builtin != null) + .component + } + row { + val detectFromPath = service().detectFromPath() + detectRadio = radioButton("Auto-detected (${detectFromPath ?: "Not found"})") + .bindSelected( + { settings.location == ClangFormatToUse.AUTO_DETECT }, + { settings.location = ClangFormatToUse.AUTO_DETECT }) + .onChanged { resetVersion() } + .enabled(detectFromPath != null) + .component + } + row { + customRadio = radioButton("Custom:") + .bindSelected({ settings.location == ClangFormatToUse.CUSTOM }, { settings.location = ClangFormatToUse.CUSTOM }) + .gap(RightGap.SMALL) + .onChanged { resetVersion() } + .component + + pathField = textFieldWithBrowseButton("Clang-Format Path") + .align(AlignX.FILL) + .validationOnInput { + val path = Paths.get(it.text.trim()) + if (path.pathString.isBlank()) { + return@validationOnInput ValidationInfo("Path cannot be empty") + } + if (!Files.exists(path)) { + return@validationOnInput ValidationInfo("Path does not exist") + } + if (!Files.isRegularFile(path)) { + return@validationOnInput ValidationInfo("Path is not a file") + } + if (!Files.isExecutable(path)) { + return@validationOnInput ValidationInfo("Path is not executable") + } + return@validationOnInput null + } + .bindText({ settings.customPath ?: "" }, { settings.customPath = it }) + .onChanged { resetVersion() } + .enabledIf(customRadio.selected) + } } row { button("Test", ::testExe) } row { - version = label("") + version = JTextArea().apply { + isEditable = false + border = null + lineWrap = true + wrapStyleWord = true + background = null + toolTipText = "Click 'Test' to check the path" + isVisible = false + font = JBFont.label() + } + cell(version) + .align(AlignX.FILL) } } } } + private fun resetVersion() { + version.text = "" + version.isVisible = false + } + private fun testExe(action: ActionEvent) { - var path = pathField.component.text + version.isVisible = true + var path = getFinalPath() ?: "" if (path.isBlank()) { - path = service().detectFromPath() ?: "" - } - if (path.isBlank()) { - version.component.text = "" + version.text = "Invalid path" return } try { - version.component.text = "Using " + service().validatePath(path) + val versionStr = service().validatePath(path) + version.text = "$versionStr\n\n$path" } catch (e: ClangFormatError) { - version.component.text = "Invalid path '$path': ${e.message}" + version.text = "${e.message}\n\n$path" + } + } + + private fun getFinalPath(): String? { + if (builtinRadio.isSelected) { + return service().getBuiltinPath()?.path + } + if (detectRadio.isSelected) { + return service().detectFromPath() } + return pathField.component.text.trim() } } diff --git a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatService.kt b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatService.kt index f977f95..8cba025 100644 --- a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatService.kt +++ b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatService.kt @@ -2,6 +2,7 @@ package com.github.aarcangeli.ideaclangformat.services import com.github.aarcangeli.ideaclangformat.exceptions.ClangFormatError import com.intellij.openapi.project.Project +import com.intellij.openapi.util.ModificationTracker import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiFile import com.intellij.util.concurrency.annotations.RequiresEdt @@ -20,12 +21,24 @@ interface ClangFormatService { */ fun mayBeFormatted(file: PsiFile, inCaseOfStyleError: Boolean): Boolean + /** + * Extract clang-format to a temporary directory and return the path to the binary. + */ + fun getBuiltinPath(): BuiltinPath? + + /** + * Returns a tracker that is invalidated if a new path is detected. + */ + fun getBuiltinPathTracker(): ModificationTracker + /** * A list of possible paths to the clang-format binary. */ fun detectFromPath(): String? - @get:Throws(ClangFormatError::class) + /** + * The path to the clang-format binary (based on the configuration). + */ val clangFormatPath: String? /** @@ -54,3 +67,5 @@ interface ClangFormatService { const val GROUP_ID = "aarcangeli.notification.ClangFormat" } } + +data class BuiltinPath(val path: String, val version: String) diff --git a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatServiceImpl.kt b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatServiceImpl.kt index c256cc4..51b8678 100644 --- a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatServiceImpl.kt +++ b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatServiceImpl.kt @@ -1,6 +1,7 @@ package com.github.aarcangeli.ideaclangformat.services import com.github.aarcangeli.ideaclangformat.ClangFormatConfig +import com.github.aarcangeli.ideaclangformat.ClangFormatToUse import com.github.aarcangeli.ideaclangformat.MyBundle.message import com.github.aarcangeli.ideaclangformat.exceptions.ClangExitCode import com.github.aarcangeli.ideaclangformat.exceptions.ClangFormatError @@ -36,6 +37,9 @@ import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.containers.ContainerUtil import java.io.File import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption import java.util.* import java.util.concurrent.atomic.AtomicReference @@ -49,6 +53,7 @@ private const val BULK_REPLACE_OPTIMIZATION_CRITERIA = 1000 class ClangFormatServiceImpl : ClangFormatService, Disposable { private val errorNotification = AtomicReference() private val afterWriteActionFinished = ContainerUtil.createLockFreeCopyOnWriteList() + private val tracker: DefaultModificationTracker = DefaultModificationTracker() init { ApplicationManager.getApplication().addApplicationListener(MyApplicationListener(), this) @@ -75,6 +80,44 @@ class ClangFormatServiceImpl : ClangFormatService, Disposable { } } + override fun getBuiltinPath(): BuiltinPath? { + val tempDir = Paths.get(PathManager.getPluginTempPath(), "clang-format-tools") + val version = ClangFormatCommons.readBuiltInVersion() + val versionMarkerString = "version-$version-${SystemInfo.OS_NAME}-${SystemInfo.OS_ARCH}" + val versionMarker = tempDir.resolve("version.txt") + + val outputFilename = if (SystemInfo.isWindows) "clang-format.exe" else "clang-format" + val outputFile = tempDir.resolve(outputFilename) + + val currentVersion = runCatching { versionMarker.toFile().readText() } + + // Check if the file exists + if (Files.exists(outputFile) && Files.isExecutable(outputFile) && currentVersion.isSuccess && currentVersion.getOrNull() == versionMarkerString) { + return BuiltinPath(outputFile.toString(), version) + } + + // Copy the file + val inputStream = ClangFormatCommons.getClangFormatPathFromResources() ?: return null + Files.createDirectories(tempDir) + Files.copy(inputStream, outputFile, StandardCopyOption.REPLACE_EXISTING) + + // Make the file executable + if (!SystemInfo.isWindows) { + outputFile.toFile().setExecutable(true) + } + + // Write the version + versionMarker.toFile().writeText(versionMarkerString) + + tracker.incModificationCount() + + return BuiltinPath(outputFile.toString(), version) + } + + override fun getBuiltinPathTracker(): ModificationTracker { + return tracker + } + override fun reformatInBackground(project: Project, virtualFile: VirtualFile) { // remove last error notification clearErrorNotification() @@ -235,6 +278,12 @@ class ClangFormatServiceImpl : ClangFormatService, Disposable { if (output.exitCode != 0) { throw ClangExitCode(output.exitCode) } + + // Check if the output contains "clang-format" + if (!output.stdout.contains("clang-format")) { + throw ClangFormatError("Invalid clang-format path") + } + return output.stdout.trim() } catch (e: ExecutionException) { @@ -242,15 +291,29 @@ class ClangFormatServiceImpl : ClangFormatService, Disposable { } } - @get:Throws(ClangFormatError::class) override val clangFormatPath: String? get() { val config = service() - if (!config.state.customPath.isNullOrBlank()) { - return config.state.customPath!!.trim() + var path: String? = null + when (config.state.location) { + ClangFormatToUse.BUILTIN -> { + val builtinPath = getBuiltinPath() + if (builtinPath != null) { + path = builtinPath.path + } + } + + ClangFormatToUse.CUSTOM -> { + if (!config.state.customPath.isNullOrBlank()) { + path = config.state.customPath!!.trim() + } + } + + ClangFormatToUse.AUTO_DETECT -> { + path = detectFromPath() + } } - val path = detectFromPath() - if (path != null) { + if (path != null && File(path).exists() && File(path).canExecute()) { return path } return null diff --git a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatStyleService.kt b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatStyleService.kt index 72d3edc..72c7622 100644 --- a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatStyleService.kt +++ b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatStyleService.kt @@ -26,7 +26,7 @@ interface ClangFormatStyleService { fun getRawFormatStyle(psiFile: PsiFile): Map /** - * Returns a tracker that should be invalidated when the .clang-format file changes. + * Returns a tracker that is invalidated when any .clang-format associated with the file is changed. * This method also covers the case when the .clang-format file is added or removed. */ @RequiresReadLock diff --git a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatStyleServiceImpl.kt b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatStyleServiceImpl.kt index cbde51b..a3f416e 100644 --- a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatStyleServiceImpl.kt +++ b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/services/ClangFormatStyleServiceImpl.kt @@ -1,12 +1,12 @@ package com.github.aarcangeli.ideaclangformat.services +import com.github.aarcangeli.ideaclangformat.ClangFormatConfig import com.github.aarcangeli.ideaclangformat.MyBundle import com.github.aarcangeli.ideaclangformat.exceptions.ClangFormatError import com.github.aarcangeli.ideaclangformat.exceptions.ClangFormatNotFound import com.github.aarcangeli.ideaclangformat.utils.ClangFormatCommons import com.github.aarcangeli.ideaclangformat.utils.ProcessUtils import com.intellij.execution.ExecutionException -import com.intellij.execution.configurations.PathEnvironmentVariableUtil import com.intellij.openapi.Disposable import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger @@ -48,14 +48,10 @@ class ClangFormatStyleServiceImpl : ClangFormatStyleService, Disposable { VirtualFileManager.getInstance().addAsyncFileListener(CacheFileWatcher(), this) } - private fun findClangFormatPath(): File? { - return PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("clang-format") - } - private fun getClangFormatVirtualPath(): VirtualFile? { - val clangFormatPath = findClangFormatPath() + val clangFormatPath = service().clangFormatPath if (clangFormatPath != null) { - return VfsUtil.findFileByIoFile(clangFormatPath, true) + return VfsUtil.findFileByIoFile(File(clangFormatPath), true) } return null } @@ -164,7 +160,7 @@ class ClangFormatStyleServiceImpl : ClangFormatStyleService, Disposable { return cachedValue } - private class FormatStyleProvider(private val service: ClangFormatStyleServiceImpl, private val psiFile: PsiFile) : + private class FormatStyleProvider(private val styleService: ClangFormatStyleServiceImpl, private val psiFile: PsiFile) : CachedValueProvider?, ClangFormatError?>> { override fun compute(): CachedValueProvider.Result?, ClangFormatError?>> { val dependencies: MutableList = ArrayList() @@ -178,16 +174,27 @@ class ClangFormatStyleServiceImpl : ClangFormatStyleService, Disposable { } private fun computeFormat(dependencies: MutableList): Map { - dependencies.add(service.makeDependencyTracker(psiFile)) + // Invalidate the cache when the configuration changes + dependencies.add(ModificationTracker { + service().stateModificationCount + }) + dependencies.add(service().getBuiltinPathTracker()) + + // Retrieve the virtual file for the psi file val virtualFile = getVirtualFile(psiFile) if (virtualFile == null) { LOG.warn("Missing filename for $psiFile") throw ClangFormatError("Cannot get clang-format configuration") } - val clangFormat = service.getClangFormatVirtualPath() ?: throw ClangFormatNotFound() - dependencies.add(clangFormat) + + // Invalidate the cache when any .clang-format file changes + dependencies.add(styleService.makeDependencyTracker(psiFile)) + + val clangFormatBin = styleService.getClangFormatVirtualPath() ?: throw ClangFormatNotFound() + dependencies.add(clangFormatBin) + try { - val commandLine = ClangFormatCommons.createCommandLine(clangFormat.path) + val commandLine = ClangFormatCommons.createCommandLine(clangFormatBin.path) commandLine.addParameter("--dump-config") commandLine.addParameter("-assume-filename=" + getFileName(virtualFile)) LOG.info("Running command: " + commandLine.commandLineString) diff --git a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/utils/ClangFormatCommons.kt b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/utils/ClangFormatCommons.kt index 115dec7..55606de 100644 --- a/src/main/kotlin/com/github/aarcangeli/ideaclangformat/utils/ClangFormatCommons.kt +++ b/src/main/kotlin/com/github/aarcangeli/ideaclangformat/utils/ClangFormatCommons.kt @@ -17,6 +17,7 @@ import com.intellij.testFramework.LightVirtualFile import com.intellij.util.PathUtil import org.jetbrains.annotations.NonNls import java.io.File +import java.io.InputStream import java.util.regex.Pattern // TODO: We should respect the user's settings for the file extensions @@ -90,6 +91,24 @@ object ClangFormatCommons { return ClangFormatError("Exit code ${output.exitCode} from ${commandLine.commandLineString}\n${stderr}") } + fun getClangFormatPathFromResources(): InputStream? { + val resourcePath = when { + SystemInfo.isWindows -> "/clang-format-win/clang-format.exe" + SystemInfo.isLinux && SystemInfo.isAarch64 -> "/clang-format-linux-aarch64/clang-format" + SystemInfo.isLinux -> "/clang-format-linux-armv7a/clang-format" + SystemInfo.isMac && SystemInfo.isAarch64 -> "/clang-format-macos-arm64/clang-format" + SystemInfo.isMac -> "/clang-format-macos-x64/clang-format" + else -> return null + } + return ClangFormatCommons::class.java.getResourceAsStream(resourcePath) + } + + fun readBuiltInVersion(): String { + val inputStream = ClangFormatCommons::class.java.getResourceAsStream("/clang-format-tag.txt") + val version = inputStream?.bufferedReader()?.use { it.readText() } ?: return "unknown" + return version.split("-")[1].trim() + } + fun createCommandLine(clangFormatPath: String): GeneralCommandLine { val commandLine = GeneralCommandLine() if (SystemInfo.isWindows && isWinShellScript(clangFormatPath)) { diff --git a/src/main/resources/clang-format-linux-aarch64/clang-format b/src/main/resources/clang-format-linux-aarch64/clang-format new file mode 100644 index 0000000..bb4df58 --- /dev/null +++ b/src/main/resources/clang-format-linux-aarch64/clang-format @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c60764d5f1426f6f11a033262a9975bed2edbf517eb32840d6aa39164c1f674 +size 4127552 diff --git a/src/main/resources/clang-format-linux-armv7a/clang-format b/src/main/resources/clang-format-linux-armv7a/clang-format new file mode 100644 index 0000000..96776b2 --- /dev/null +++ b/src/main/resources/clang-format-linux-armv7a/clang-format @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89f5e0eb994427662758dd233a1a326e5831bfb6f873d969d82e2b4dd8523602 +size 3794828 diff --git a/src/main/resources/clang-format-macos-arm64/clang-format b/src/main/resources/clang-format-macos-arm64/clang-format new file mode 100644 index 0000000..8c0b83e --- /dev/null +++ b/src/main/resources/clang-format-macos-arm64/clang-format @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d738128bee94025e7f5580a1d29ddcf89ef4c186bd7660a1f04ecefdee3e1cd +size 3672416 diff --git a/src/main/resources/clang-format-macos-x64/clang-format b/src/main/resources/clang-format-macos-x64/clang-format new file mode 100644 index 0000000..6653b66 --- /dev/null +++ b/src/main/resources/clang-format-macos-x64/clang-format @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:694def3f71888349f4aaad76f7edb60e39073a6b212406d132aa47b8373342c6 +size 3824672 diff --git a/src/main/resources/clang-format-tag.txt b/src/main/resources/clang-format-tag.txt new file mode 100644 index 0000000..0e5541b --- /dev/null +++ b/src/main/resources/clang-format-tag.txt @@ -0,0 +1 @@ +llvmorg-19.1.7 diff --git a/src/main/resources/clang-format-win/clang-format.exe b/src/main/resources/clang-format-win/clang-format.exe new file mode 100644 index 0000000..a8ce06f --- /dev/null +++ b/src/main/resources/clang-format-win/clang-format.exe @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7b393b46257cc375d5832a44ff37fe52cf64f60a92880e6977fca5227810a20 +size 3685888