Skip to content

Commit

Permalink
Use built in clang-format + fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
aarcangeli committed Feb 22, 2025
1 parent 966e1c5 commit b5a2501
Show file tree
Hide file tree
Showing 15 changed files with 248 additions and 44 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.exe filter=lfs diff=lfs merge=lfs -text
clang-format filter=lfs diff=lfs merge=lfs -text
3 changes: 2 additions & 1 deletion .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,18 @@ class ClangFormatConfig : SimplePersistentStateComponent<ClangFormatConfig.State
class State : BaseState() {
var enabled by property(true)

var location by enum(ClangFormatToUse.BUILTIN)

var formatOnSave by property(false)

/// The path to the clang-format executable.
/// If null, the plugin will try to find it in the PATH.
var customPath by string()
}
}

enum class ClangFormatToUse {
BUILTIN,
CUSTOM,
AUTO_DETECT
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.aarcangeli.ideaclangformat.configurable

import com.github.aarcangeli.ideaclangformat.ClangFormatConfig
import com.github.aarcangeli.ideaclangformat.ClangFormatToUse
import com.github.aarcangeli.ideaclangformat.exceptions.ClangFormatError
import com.github.aarcangeli.ideaclangformat.services.ClangFormatService
import com.github.aarcangeli.ideaclangformat.utils.ClangFormatCommons
Expand All @@ -11,27 +12,38 @@ import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.psi.codeStyle.CodeStyleSettingsManager
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.dsl.builder.*
import com.intellij.ui.layout.selected
import com.intellij.util.ui.JBFont
import java.awt.event.ActionEvent
import javax.swing.JLabel
import java.nio.file.Files
import java.nio.file.Paths
import javax.swing.JRadioButton
import javax.swing.JTextArea
import kotlin.io.path.pathString

class AppConfigurable : DslConfigurableBase(), SearchableConfigurable, NoScroll {
private lateinit var combobox: Cell<JBCheckBox>

private val settings = service<ClangFormatConfig>().state
private lateinit var version: Cell<JLabel>
private lateinit var version: JTextArea
private lateinit var pathField: Cell<TextFieldWithBrowseButton>

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 {
Expand All @@ -53,44 +65,105 @@ class AppConfigurable : DslConfigurableBase(), SearchableConfigurable, NoScroll
}

group("Location") {
row("Auto-detected:") {
val detectFromPath = service<ClangFormatService>().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<ClangFormatService>().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<ClangFormatService>().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<ClangFormatService>().detectFromPath() ?: ""
}
if (path.isBlank()) {
version.component.text = ""
version.text = "Invalid path"
return
}
try {
version.component.text = "Using " + service<ClangFormatService>().validatePath(path)
val versionStr = service<ClangFormatService>().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<ClangFormatService>().getBuiltinPath()?.path
}
if (detectRadio.isSelected) {
return service<ClangFormatService>().detectFromPath()
}
return pathField.component.text.trim()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?

/**
Expand Down Expand Up @@ -54,3 +67,5 @@ interface ClangFormatService {
const val GROUP_ID = "aarcangeli.notification.ClangFormat"
}
}

data class BuiltinPath(val path: String, val version: String)
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand All @@ -49,6 +53,7 @@ private const val BULK_REPLACE_OPTIMIZATION_CRITERIA = 1000
class ClangFormatServiceImpl : ClangFormatService, Disposable {
private val errorNotification = AtomicReference<Notification?>()
private val afterWriteActionFinished = ContainerUtil.createLockFreeCopyOnWriteList<Runnable>()
private val tracker: DefaultModificationTracker = DefaultModificationTracker()

init {
ApplicationManager.getApplication().addApplicationListener(MyApplicationListener(), this)
Expand All @@ -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()
Expand Down Expand Up @@ -235,22 +278,42 @@ 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) {
throw ClangFormatError("Invalid clang-format path")
}
}

@get:Throws(ClangFormatError::class)
override val clangFormatPath: String?
get() {
val config = service<ClangFormatConfig>()
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface ClangFormatStyleService {
fun getRawFormatStyle(psiFile: PsiFile): Map<String, Any>

/**
* 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
Expand Down
Loading

0 comments on commit b5a2501

Please sign in to comment.