Skip to content

Commit

Permalink
Choose a custom path
Browse files Browse the repository at this point in the history
  • Loading branch information
aarcangeli committed Nov 1, 2024
1 parent 348b4bb commit 6cfefe0
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 68 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@ To use the plugin, you need to have `clang-format` installed on your system.
You can install clang-format together with [LLVM](https://github.com/llvm/llvm-project/releases) or separately using a package manager.

```bash
# All platforms
# Using PIP (all platforms)
pip install clang-format
# Using NPM (all platforms)
npm install -g clang-format
# ubuntu
# Using APT (Linux)
sudo apt-get install clang-format
# Windows
choco install llvm
# macOS
# Using Homebrew (macOS)
brew install clang-format
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@ package com.github.aarcangeli.ideaclangformat.configurable

import com.github.aarcangeli.ideaclangformat.ClangFormatConfig
import com.github.aarcangeli.ideaclangformat.MyBundle
import com.github.aarcangeli.ideaclangformat.exceptions.ClangFormatError
import com.github.aarcangeli.ideaclangformat.services.ClangFormatService
import com.intellij.openapi.components.service
import com.intellij.openapi.options.Configurable.NoScroll
import com.intellij.openapi.options.DslConfigurableBase
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.*
import java.awt.event.ActionEvent
import javax.swing.JLabel

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 pathField: Cell<TextFieldWithBrowseButton>

override fun getId(): String = "aarcangeli.ideaclangformat.appconfig"
override fun getDisplayName(): String = "Clang-Format Tools"
Expand All @@ -38,6 +42,46 @@ class AppConfigurable : DslConfigurableBase(), SearchableConfigurable, NoScroll
combobox = checkBox("Format on save")
.bindSelected(settings::formatOnSave)
}

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.path ?: "" }, { settings.path = it })
}
row {
button("Test", ::testExe)
}
row {
version = label("")
}
}
}
}

private fun testExe(action: ActionEvent) {
var path = pathField.component.text
if (path.isBlank()) {
path = service<ClangFormatService>().detectFromPath() ?: ""
}
if (path.isBlank()) {
version.component.text = ""
return
}
try {
version.component.text = "Using " + service<ClangFormatService>().validatePath(path)
}
catch (e: ClangFormatError) {
version.component.text = "Invalid path '$path': ${e.message}"
}
}
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.github.aarcangeli.ideaclangformat.exceptions

import com.github.aarcangeli.ideaclangformat.MyBundle
import com.intellij.build.FileNavigatable
import com.intellij.pom.Navigatable
import org.jetbrains.annotations.Nls

open class ClangFormatError : RuntimeException {
constructor(message: @Nls String) : super(message)
constructor(message: @Nls String, cause: Throwable?) : super(message, cause)
}

class ClangFormatNotFound : ClangFormatError(MyBundle.message("error.clang-format.error.not-found"))

/**
* Error when clang-format reports an error on the style file.
*/
class ClangValidationError(@JvmField val description: @Nls String, private val fileNavigatable: FileNavigatable) :
ClangFormatError(description) {

fun getFileNavigatable(): Navigatable {
return fileNavigatable
}
}

/**
* Thrown on any exit code different from 0.
*/
class ClangExitCode(val exitCode: Int) : ClangFormatError("Clang-format exited with code $exitCode")

/**
* Thrown when no ".clang-format" files support the file language.
* eg: "Configuration file(s) do(es) not support CSharp: /path/.clang-format"
*/
class ClangMissingLanguageException(message: @Nls String) : ClangFormatError(message)
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ package com.github.aarcangeli.ideaclangformat.experimental

import com.github.aarcangeli.ideaclangformat.ClangFormatConfig
import com.github.aarcangeli.ideaclangformat.MyBundle
import com.github.aarcangeli.ideaclangformat.configurable.AppConfigurable
import com.github.aarcangeli.ideaclangformat.exceptions.ClangExitCodeError
import com.github.aarcangeli.ideaclangformat.exceptions.ClangValidationError
import com.github.aarcangeli.ideaclangformat.exceptions.ClangFormatError
import com.github.aarcangeli.ideaclangformat.services.ClangFormatService
import com.github.aarcangeli.ideaclangformat.services.ClangFormatStyleService
import com.github.aarcangeli.ideaclangformat.utils.ClangFormatCommons
import com.intellij.CodeStyleBundle
import com.intellij.application.options.CodeStyle
import com.intellij.icons.AllIcons
import com.intellij.ide.actions.ShowSettingsUtilImpl
import com.intellij.ide.util.PsiNavigationSupport
Expand Down Expand Up @@ -67,7 +65,7 @@ class ClangFormatStyleSettingsModifier : CodeStyleSettingsModifier {
clangFormatStyle.apply(settings)
return true
}
catch (e: ClangExitCodeError) {
catch (e: ClangValidationError) {
// configuration broken, re-use last provided settings
val clangFormatStyle = file.getUserData(LAST_PROVIDED_SETTINGS)
if (clangFormatStyle != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ interface ClangFormatService {
*/
fun mayBeFormatted(file: PsiFile): Boolean

/**
* A list of possible paths to the clang-format binary.
*/
fun detectFromPath(): String?

@get:Throws(ClangFormatError::class)
val clangFormatPath: String
val clangFormatPath: String?

/**
* Reformat the specified file
Expand All @@ -40,6 +45,11 @@ interface ClangFormatService {
*/
fun clearErrorNotification()

/**
* @throws ClangFormatError
*/
fun validatePath(path: String): String

companion object {
const val GROUP_ID = "aarcangeli.notification.ClangFormat"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package com.github.aarcangeli.ideaclangformat.services

import com.github.aarcangeli.ideaclangformat.ClangFormatConfig
import com.github.aarcangeli.ideaclangformat.MyBundle.message
import com.github.aarcangeli.ideaclangformat.exceptions.ClangExitCodeError
import com.github.aarcangeli.ideaclangformat.exceptions.ClangExitCode
import com.github.aarcangeli.ideaclangformat.exceptions.ClangValidationError
import com.github.aarcangeli.ideaclangformat.exceptions.ClangFormatError
import com.github.aarcangeli.ideaclangformat.utils.ClangFormatCommons
import com.github.aarcangeli.ideaclangformat.utils.OffsetConverter
Expand Down Expand Up @@ -165,7 +166,7 @@ class ClangFormatServiceImpl : ClangFormatService, Disposable {
catch (e: ProcessCanceledException) {
null
}
catch (e: ClangExitCodeError) {
catch (e: ClangValidationError) {
LOG.warn("Cannot format document", e)
showFormatError(
project,
Expand Down Expand Up @@ -220,20 +221,41 @@ class ClangFormatServiceImpl : ClangFormatService, Disposable {
errorNotification.getAndSet(null)?.expire()
}

override fun validatePath(path: String): String {
val file = File(path)
if (!file.exists() || !file.canExecute()) {
throw ClangFormatError("Invalid clang-format path")
}
LOG.info("Validating path: $path")
val commandLine = ClangFormatCommons.createCommandLine(path)
commandLine.addParameter("--version")
try {
val output = ProcessUtils.executeProgram(commandLine, ByteArray(0))
LOG.info("Output: ${output.stdout}")
if (output.exitCode != 0) {
throw ClangExitCode(output.exitCode)
}
return output.stdout.trim()
}
catch (e: ExecutionException) {
throw ClangFormatError("Invalid clang-format path")
}
}

@get:Throws(ClangFormatError::class)
override val clangFormatPath: String
override val clangFormatPath: String?
get() {
val path = findClangFormatPath()
if (path == null || !path.canExecute()) {
throw ClangFormatError("Cannot find clang-format")
val config = service<ClangFormatConfig>()
if (!config.state.path.isNullOrBlank()) {
return config.state.path!!.trim()
}
return path.absolutePath
val path = detectFromPath()
if (path != null) {
return path
}
return null
}

private fun findClangFormatPath(): File? {
return PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("clang-format")
}

override fun mayBeFormatted(file: PsiFile): Boolean {
if (!service<ClangFormatConfig>().state.enabled) {
return false
Expand All @@ -251,7 +273,7 @@ class ClangFormatServiceImpl : ClangFormatService, Disposable {
val formatStyle = try {
formatStyleService.getRawFormatStyle(file)
}
catch (e: ClangExitCodeError) {
catch (e: ClangValidationError) {
// the configuration file contains errors
return true
}
Expand All @@ -271,11 +293,20 @@ class ClangFormatServiceImpl : ClangFormatService, Disposable {
return true
}

override fun detectFromPath(): String? {
val path = PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("clang-format")
if (path != null && path.canExecute()) {
return path.absolutePath
}
return null
}

override fun dispose() {}

@Throws(ExecutionException::class)
private fun executeClangFormat(project: Project, content: ByteArray, filename: String): ClangFormatResponse {
val commandLine = ClangFormatCommons.createCompileCommand(clangFormatPath)
val path = clangFormatPath ?: throw ClangFormatError("Cannot find clang-format")
val commandLine = ClangFormatCommons.createCommandLine(path)
commandLine.addParameter("-output-replacements-xml")
commandLine.addParameter("-assume-filename=$filename")
LOG.info("Running command: " + commandLine.commandLineString)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,10 @@ class ClangFormatStyleServiceImpl : ClangFormatStyleService, Disposable {
LOG.warn("Missing filename for $psiFile")
throw ClangFormatError("Cannot get clang-format configuration")
}
val clangFormat = service.getClangFormatVirtualPath()
?: throw ClangFormatNotFound(MyBundle.message("error.clang-format.error.not-found"))
val clangFormat = service.getClangFormatVirtualPath() ?: throw ClangFormatNotFound()
dependencies.add(clangFormat)
try {
val commandLine = ClangFormatCommons.createCompileCommand(clangFormat.path)
val commandLine = ClangFormatCommons.createCommandLine(clangFormat.path)
commandLine.addParameter("--dump-config")
commandLine.addParameter("-assume-filename=" + getFileName(virtualFile))
LOG.info("Running command: " + commandLine.commandLineString)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.github.aarcangeli.ideaclangformat.utils

import com.github.aarcangeli.ideaclangformat.exceptions.ClangExitCodeError
import com.github.aarcangeli.ideaclangformat.exceptions.ClangValidationError
import com.github.aarcangeli.ideaclangformat.exceptions.ClangFormatError
import com.github.aarcangeli.ideaclangformat.exceptions.ClangMissingLanguageException
import com.github.aarcangeli.ideaclangformat.services.ClangFormatService
Expand Down Expand Up @@ -58,7 +58,7 @@ object ClangFormatCommons {
filename.equals("_clang-format", ignoreCase = true)
}

@Throws(ClangExitCodeError::class)
@Throws(ClangValidationError::class)
fun getException(project: Project, commandLine: GeneralCommandLine, output: ProcessOutput): ClangFormatError {
var stderr = output.stderr
if (stderr.startsWith("Configuration file(s) do(es) not support")) {
Expand All @@ -77,7 +77,7 @@ object ClangFormatCommons {
${fileName.name}:$lineNumber:$column: $message
${stderr.substring(matcher.group(0).length).trim { it <= ' ' }}
""".trimIndent()
return ClangExitCodeError(
return ClangValidationError(
description,
FileNavigatable(project, FilePosition(fileName, lineNumber - 1, column - 1))
)
Expand All @@ -94,7 +94,7 @@ object ClangFormatCommons {
return ClangFormatError("Exit code ${output.exitCode} from ${commandLine.commandLineString}\n${stderr}")
}

fun createCompileCommand(clangFormatPath: String): GeneralCommandLine {
fun createCommandLine(clangFormatPath: String): GeneralCommandLine {
val commandLine = GeneralCommandLine()
if (SystemInfo.isWindows && isWinShellScript(clangFormatPath)) {
commandLine.exePath = "cmd.exe"
Expand Down

0 comments on commit 6cfefe0

Please sign in to comment.