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