mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-30757 Black formatter integration support for PyCharm
Merge-request: IJ-MR-107500 Merged-by: Daniil Kalinin <Daniil.Kalinin@jetbrains.com> GitOrigin-RevId: f0e622057a9533d89d2d47b032b5388f8c991821
This commit is contained in:
committed by
intellij-monorepo-bot
parent
a834a7647e
commit
62df4deaf4
@@ -1373,4 +1373,50 @@ python.sdk.configurable.name=Python SDK
|
||||
inlay.hints.usages.text={0,choice, 0#no usages|1#1 usage|2#{0,number}'{1,choice, |1#+}' usages}
|
||||
inlay.hints.usages.with.dynamic.text={0,choice, 0#no usages|1#1 usage|2#{0,number} usages} ({1,choice, 1#1 dynamic|2#{1,number} dynamic})
|
||||
|
||||
|
||||
# Black formatter
|
||||
black.configurable.name=Black
|
||||
black.formatting.service.name=Black formatter
|
||||
black.not.installed.error=Black formatter package is not installed on the current interpreter
|
||||
black.no.project.interpreter.error=No Python interpreter configured for the project
|
||||
black.remote.sdk.error=Package mode is not available for remote SDKs. Please use Binary mode.
|
||||
black.install.button.label=Install Black
|
||||
black.use.section.label=Use Black formatter:
|
||||
black.enable.black.checkbox.label=On code reformat
|
||||
black.enable.action.on.save.label=On save
|
||||
black.advanced.settings.panel.title=Advanced settings
|
||||
black.empty.output.error=Black formatter returned empty output
|
||||
black.empty.path.to.executable.exception.text=Black executable not found or empty
|
||||
black.action.on.save.name=Run Black
|
||||
black.action.on.save.package.info=Using Black package v{0}
|
||||
black.action.on.save.executable.info=Using Black executable v{0}
|
||||
black.action.on.save.executable.path.not.specified=Black executable path not specified
|
||||
black.formatting.with.black=Formatting with Black
|
||||
black.no.lines.changed=Black: No lines changed. Content is already properly formatted.
|
||||
black.formatted.n.lines=Black: Formatted {0} {1, choice, 0#lines|1#line}
|
||||
black.installing.modal.title=Installing Black formatter
|
||||
black.installation.error.title=Failed to install Black formatter
|
||||
black.failed.to.format.on.save.error.label=Black: Failed to format file {0}
|
||||
black.processing.file.name=Black: Processing file {0}
|
||||
black.exception.error.message=Black formatter error
|
||||
black.file.ignored.notification.label=Black: File ignored
|
||||
black.file.ignored.notification.message=File {0} ignored according to --exclude or --force-exclude rules
|
||||
black.advertising.service.notification.title=Black formatter integration
|
||||
black.advertising.service.found.in.packages=Black formatter package is detected on the project interpreter. Try Black formatter integration!
|
||||
black.advertising.service.found.in.PATH=Black formatter executable is detected in {0, choice, 0#%PATH%|1#$PATH}. Try Black formatter integration!
|
||||
black.advertising.service.configure.button.label=Configure
|
||||
black.advertising.service.dont.show.again.label=Don't show again
|
||||
black.select.path.to.executable=Select path to Black formatter executable
|
||||
black.execution.mode.label=Execution mode:
|
||||
black.executable.label=Black executable:
|
||||
black.executable.auto.detected.path=Auto-detected: {0}
|
||||
black.executable.not.found=Unable to auto-detect Black executable in {0, choice, 0#%PATH%|1#$PATH}
|
||||
black.execution.mode.package=Package
|
||||
black.execution.mode.binary=Binary
|
||||
black.execution.mode.tooltip.text=Use either executable or package installed on the project SDK
|
||||
black.cli.args.text.field.label=Settings:
|
||||
black.cli.args.comment=List command line arguments separated by whitespace. <a href='https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#command-line-options'>Learn more</a>
|
||||
black.cli.args.validation.notification=No value passed for parameter {0}
|
||||
black.sdk.not.configured.error=No project SDK configured for the project {0}
|
||||
black.sdk.not.configured.error.title=SDK not configured
|
||||
black.remote.sdk.exception.text=Black formatter invocation in Package mode is not allowed on remote SDKs
|
||||
black.sdk.selection.combobox.label=Select Python SDK:
|
||||
|
||||
@@ -154,8 +154,8 @@ envs {
|
||||
|
||||
createPython("python3.11",
|
||||
python311version,
|
||||
[],
|
||||
"python3.11",
|
||||
["black == 23.1.0"],
|
||||
"python3.11\nblack",
|
||||
true)
|
||||
|
||||
createPython("python3.12",
|
||||
|
||||
@@ -512,6 +512,29 @@
|
||||
|
||||
<registryKey key="python.new.run.config" defaultValue="true" description="Python Run configurations new UI"/>
|
||||
|
||||
<!-- Black -->
|
||||
<projectConfigurable groupId="tools"
|
||||
provider="com.jetbrains.python.black.configuration.BlackFormatterConfigurableProvider"
|
||||
id="com.jetbrains.python.black.configuration.BlackFormatterConfigurable"
|
||||
bundle="messages.PyBundle"
|
||||
key="black.configurable.name"
|
||||
nonDefaultProject="true"/>
|
||||
|
||||
<registryKey key="black.formatter.support.enabled" defaultValue="false"
|
||||
description="Enable Black formatter support"/>
|
||||
|
||||
<formattingService implementation="com.jetbrains.python.black.BlackFormattingService"/>
|
||||
<actionOnSave id="BlackFormatterActionOnSave"
|
||||
implementation="com.jetbrains.python.black.BlackFormatterActionOnSave"
|
||||
order="after FormatOnSaveAction"/>
|
||||
|
||||
<actionOnSaveInfoProvider id="BlackFormatterActionOnSaveInfo"
|
||||
implementation="com.jetbrains.python.black.configuration.BlackFormatterConfigurable$BlackFormatterActionOnSaveInfoProvider"/>
|
||||
|
||||
<projectService serviceImplementation="com.jetbrains.python.black.configuration.BlackFormatterConfiguration"/>
|
||||
<notificationGroup id="Black Formatter Integration" displayType="STICKY_BALLOON" isLogByDefault="true" bundle="messages.PyBundle"
|
||||
key="black.configurable.name"/>
|
||||
|
||||
<dependencyCollector kind="python" implementation="com.jetbrains.python.packaging.PyDependencyCollector"/>
|
||||
</extensions>
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.black
|
||||
|
||||
import com.intellij.ide.actionsOnSave.impl.ActionsOnSaveFileDocumentManagerListener.ActionOnSave
|
||||
import com.intellij.notification.Notification
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.notification.Notifications
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.command.WriteCommandAction
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.progress.progressStep
|
||||
import com.intellij.openapi.progress.runBlockingModal
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.black.configuration.BlackFormatterConfiguration
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.Nls
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class BlackFormatterActionOnSave : ActionOnSave() {
|
||||
|
||||
companion object {
|
||||
val LOG = thisLogger()
|
||||
}
|
||||
|
||||
override fun isEnabledForProject(project: Project): Boolean = Registry.`is`("black.formatter.support.enabled")
|
||||
|
||||
override fun processDocuments(project: Project, documents: Array<Document?>) {
|
||||
val blackConfig = BlackFormatterConfiguration.getBlackConfiguration(project)
|
||||
if (!blackConfig.enabledOnSave) return
|
||||
|
||||
val sdk = blackConfig.getSdk(project)
|
||||
if (sdk == null) {
|
||||
LOG.warn(PyBundle.message("black.sdk.not.configured.error", project.name))
|
||||
return
|
||||
}
|
||||
|
||||
formatMultipleDocuments(project, sdk, blackConfig, documents.filterNotNull().toList())
|
||||
}
|
||||
|
||||
private fun formatMultipleDocuments(project: Project,
|
||||
sdk: Sdk,
|
||||
blackConfig: BlackFormatterConfiguration,
|
||||
documents: List<Document>) {
|
||||
val manager = FileDocumentManager.getInstance()
|
||||
|
||||
val executor = try {
|
||||
BlackFormatterExecutor(project, sdk, blackConfig)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
reportFailure(PyBundle.message("black.exception.error.message"), e.localizedMessage, project)
|
||||
return
|
||||
}
|
||||
|
||||
val descriptors = documents
|
||||
.mapNotNull { document -> manager.getFile(document)?.let { document to it } }
|
||||
.filter { BlackFormatterUtil.isFileApplicable(it.second) }
|
||||
.map { Descriptor(it.first, it.second) }
|
||||
|
||||
runCatching {
|
||||
runBlockingModal(project, PyBundle.message("black.formatting.with.black")) {
|
||||
var processedFiles = 0L
|
||||
|
||||
descriptors.forEach { descriptor ->
|
||||
processedFiles++
|
||||
progressStep(processedFiles / descriptors.size.toDouble(),
|
||||
PyBundle.message("black.processing.file.name", descriptor.virtualFile.name)) {
|
||||
val request = BlackFormattingRequest.File(descriptor.document.text, descriptor.virtualFile)
|
||||
val response = executor.getBlackFormattingResponse(request, BlackFormatterExecutor.BLACK_DEFAULT_TIMEOUT)
|
||||
applyChanges(project, descriptor, response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure { exception ->
|
||||
when (exception) {
|
||||
is CancellationException -> { /* ignore */ }
|
||||
else -> {
|
||||
LOG.warn(exception)
|
||||
reportFailure(PyBundle.message("black.exception.error.message"), exception.localizedMessage, project)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun applyChanges(project: Project, descriptor: Descriptor, response: BlackFormattingResponse) {
|
||||
when (response) {
|
||||
is BlackFormattingResponse.Success -> {
|
||||
withContext(Dispatchers.EDT) {
|
||||
WriteCommandAction
|
||||
.runWriteCommandAction(project,
|
||||
null,
|
||||
null,
|
||||
{ descriptor.document.setText(response.formattedText) })
|
||||
}
|
||||
}
|
||||
is BlackFormattingResponse.Failure -> {
|
||||
reportFailure(response.title, response.description, project)
|
||||
}
|
||||
is BlackFormattingResponse.Ignored -> {
|
||||
reportIgnored(response.title, response.description, project)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reportFailure(@Nls title: String, @Nls message: String, project: Project) {
|
||||
Notifications.Bus.notify(
|
||||
Notification(BlackFormattingService.NOTIFICATION_GROUP_ID,
|
||||
title,
|
||||
message, NotificationType.ERROR), project)
|
||||
}
|
||||
|
||||
// [TODO] add `do not show again` option
|
||||
private fun reportIgnored(@Nls title: String, @Nls message: String, project: Project) {
|
||||
Notifications.Bus.notify(
|
||||
Notification(BlackFormattingService.NOTIFICATION_GROUP_ID,
|
||||
title,
|
||||
message, NotificationType.INFORMATION), project)
|
||||
}
|
||||
|
||||
private data class Descriptor(val document: Document, val virtualFile: VirtualFile)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.black
|
||||
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.notification.NotificationAction
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.options.ShowSettingsUtil
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.black.configuration.BlackFormatterConfigurable
|
||||
import com.jetbrains.python.black.configuration.BlackFormatterConfiguration
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.NonNls
|
||||
|
||||
@ApiStatus.Internal
|
||||
@Service(Service.Level.APP)
|
||||
class BlackFormatterAdvertiserService private constructor() {
|
||||
|
||||
companion object {
|
||||
@NonNls
|
||||
const val SHOW_BLACK_FORMATTER_SUPPORT_NOTIFICATION = "black.formatter.show.support.notification"
|
||||
|
||||
fun getInstance(): BlackFormatterAdvertiserService =
|
||||
ApplicationManager.getApplication().getService(BlackFormatterAdvertiserService::class.java)
|
||||
}
|
||||
|
||||
private var alreadyShown: Boolean = false
|
||||
|
||||
fun suggestBlack(project: Project, blackFormatterConfiguration: BlackFormatterConfiguration) {
|
||||
if (blackFormatterConfiguration == BlackFormatterConfiguration() && !alreadyShown) {
|
||||
if (BlackFormatterUtil.isBlackFormatterInstalledOnProjectSdk(blackFormatterConfiguration.getSdk(project))) {
|
||||
showBlackFormatterSupportNotification(project,
|
||||
PyBundle.message("black.advertising.service.found.in.packages"))
|
||||
}
|
||||
else if (BlackFormatterUtil.isBlackExecutableDetected()) {
|
||||
showBlackFormatterSupportNotification(project,
|
||||
PyBundle.message("black.advertising.service.found.in.PATH",
|
||||
if (SystemInfo.isWindows) 0 else 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun showBlackFormatterSupportNotification(project: Project, @Nls message: String) {
|
||||
val propertiesComponent = PropertiesComponent.getInstance()
|
||||
if (!propertiesComponent.getBoolean(SHOW_BLACK_FORMATTER_SUPPORT_NOTIFICATION, true)) {
|
||||
return
|
||||
}
|
||||
|
||||
val notification = NotificationGroupManager.getInstance().getNotificationGroup(BlackFormattingService.NOTIFICATION_GROUP_ID)
|
||||
.createNotification(PyBundle.message("black.advertising.service.notification.title"), message, NotificationType.INFORMATION)
|
||||
.setDisplayId("black.formatter")
|
||||
.setSuggestionType(true)
|
||||
.setImportantSuggestion(true)
|
||||
.addAction(NotificationAction
|
||||
.createSimpleExpiring(PyBundle.message("black.advertising.service.configure.button.label")) {
|
||||
ShowSettingsUtil.getInstance().showSettingsDialog(project, BlackFormatterConfigurable::class.java)
|
||||
})
|
||||
.addAction(NotificationAction
|
||||
.createSimpleExpiring(PyBundle.message("black.advertising.service.dont.show.again.label")) {
|
||||
propertiesComponent.setValue(
|
||||
SHOW_BLACK_FORMATTER_SUPPORT_NOTIFICATION,
|
||||
"false", "true")
|
||||
})
|
||||
notification.notify(project)
|
||||
alreadyShown = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.black
|
||||
|
||||
import com.intellij.execution.process.*
|
||||
import com.intellij.execution.target.*
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironment
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.fileTypes.FileTypeRegistry
|
||||
import com.intellij.openapi.progress.util.ProgressIndicatorUtils
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.Version
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.black.configuration.BlackFormatterConfiguration
|
||||
import com.jetbrains.python.pyi.PyiFileType
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.InvalidSdkException
|
||||
import com.jetbrains.python.sdk.configureBuilderToRunPythonOnTarget
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.nio.charset.Charset
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class BlackFormatterExecutor(private val project: Project,
|
||||
private val sdk: Sdk,
|
||||
private val blackConfig: BlackFormatterConfiguration) {
|
||||
|
||||
private var targetEnvironment: TargetEnvironment
|
||||
private var targetEnvironmentRequest: TargetEnvironmentRequest
|
||||
|
||||
companion object {
|
||||
val BLACK_DEFAULT_TIMEOUT = 30000.milliseconds
|
||||
private val LOG = thisLogger()
|
||||
private val minimalStdinFilenameCompatibleVersion = Version(21, 4, 0)
|
||||
}
|
||||
|
||||
init {
|
||||
when (blackConfig.executionMode) {
|
||||
BlackFormatterConfiguration.ExecutionMode.BINARY -> {
|
||||
targetEnvironmentRequest = LocalTargetEnvironmentRequest()
|
||||
targetEnvironment = LocalTargetEnvironment(LocalTargetEnvironmentRequest())
|
||||
}
|
||||
BlackFormatterConfiguration.ExecutionMode.PACKAGE -> {
|
||||
if (!sdk.sdkType.isLocalSdk(sdk)) {
|
||||
throw InvalidSdkException(PyBundle.message("black.remote.sdk.exception.text"))
|
||||
}
|
||||
val interpreter = PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(sdk, project)
|
||||
targetEnvironmentRequest = interpreter.targetEnvironmentRequest
|
||||
targetEnvironment = targetEnvironmentRequest.prepareEnvironment(TargetProgressIndicator.EMPTY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getBlackFormattingResponse(blackFormattingRequest: BlackFormattingRequest,
|
||||
timeout: Duration): BlackFormattingResponse {
|
||||
val targetCMD = buildTargetCommandLine(blackFormattingRequest, targetEnvironmentRequest)
|
||||
val future = getFuture(targetCMD, targetEnvironment, blackFormattingRequest, timeout)
|
||||
return ProgressIndicatorUtils.awaitWithCheckCanceled(future)
|
||||
}
|
||||
|
||||
|
||||
private fun buildTargetCommandLine(blackFormattingRequest: BlackFormattingRequest,
|
||||
targetEnvRequest: TargetEnvironmentRequest): TargetedCommandLine {
|
||||
val cmd = TargetedCommandLineBuilder(targetEnvRequest)
|
||||
val cmdArgs = configToCmdArguments(blackFormattingRequest.virtualFile)
|
||||
val cwd = blackFormattingRequest.virtualFile.parent.takeIf { it.isDirectory }
|
||||
if (cwd != null) {
|
||||
cmd.setWorkingDirectory(cwd.path)
|
||||
}
|
||||
|
||||
when (blackConfig.executionMode) {
|
||||
BlackFormatterConfiguration.ExecutionMode.BINARY -> {
|
||||
val blackExecutable = blackConfig.pathToExecutable
|
||||
if (blackExecutable == null) {
|
||||
throw FileNotFoundException(PyBundle.message("black.empty.path.to.executable.exception.text"))
|
||||
}
|
||||
cmd.setExePath(blackExecutable)
|
||||
}
|
||||
BlackFormatterConfiguration.ExecutionMode.PACKAGE -> {
|
||||
sdk.configureBuilderToRunPythonOnTarget(cmd)
|
||||
cmd.addParameters("-m", BlackFormatterUtil.PACKAGE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
cmd.addParameters(cmdArgs)
|
||||
cmd.addParameter("-")
|
||||
return cmd.build()
|
||||
}
|
||||
|
||||
private fun configToCmdArguments(vFile: VirtualFile): List<String> {
|
||||
val cmd = mutableListOf<String>()
|
||||
val blackVersion = BlackFormatterVersionService.getVersion(project)
|
||||
|
||||
if (FileTypeRegistry.getInstance().isFileOfType(vFile, PyiFileType.INSTANCE)) {
|
||||
cmd.add("--pyi")
|
||||
}
|
||||
|
||||
if (blackConfig.cmdArguments.isNotEmpty()) {
|
||||
cmd.addAll(blackConfig.cmdArguments.split(" "))
|
||||
}
|
||||
|
||||
if (blackVersion >= minimalStdinFilenameCompatibleVersion) {
|
||||
cmd.add("--stdin-filename")
|
||||
cmd.add(vFile.path)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
private fun getFuture(targetCMD: TargetedCommandLine,
|
||||
targetEnvironment: TargetEnvironment,
|
||||
formattingRequest: BlackFormattingRequest,
|
||||
timeout: Duration): CompletableFuture<BlackFormattingResponse> {
|
||||
val vFile = formattingRequest.virtualFile
|
||||
|
||||
val future = CompletableFuture<BlackFormattingResponse>()
|
||||
.completeOnTimeout(BlackFormattingResponse.Failure(PyBundle.message("black.failed.to.format.on.save.error.label",
|
||||
vFile.name), "Timeout exceeded", null),
|
||||
timeout.inWholeMilliseconds, TimeUnit.MILLISECONDS)
|
||||
|
||||
val process = targetEnvironment.createProcess(targetCMD)
|
||||
|
||||
val processHandler = CapturingProcessHandler(process, BlackFormattingService.DEFAULT_CHARSET,
|
||||
targetCMD.getCommandPresentation(targetEnvironment))
|
||||
|
||||
processHandler.addProcessListener(writeToStdinListener(formattingRequest.fragmentToFormat, vFile.charset))
|
||||
|
||||
processHandler.addProcessListener(object : CapturingProcessAdapter() {
|
||||
override fun processTerminated(event: ProcessEvent) {
|
||||
val exitCode = event.exitCode
|
||||
if (exitCode == 0) {
|
||||
if (output.stdout.isEmpty()) {
|
||||
future.complete(BlackFormattingResponse.Ignored(PyBundle.message("black.file.ignored.notification.label"),
|
||||
PyBundle.message("black.file.ignored.notification.message", vFile.name)))
|
||||
}
|
||||
else {
|
||||
future.complete(BlackFormattingResponse.Success(output.stdout))
|
||||
}
|
||||
}
|
||||
else {
|
||||
future.complete(BlackFormattingResponse.Failure(
|
||||
PyBundle.message("black.failed.to.format.on.save.error.label", vFile.name),
|
||||
output.stderr, exitCode))
|
||||
}
|
||||
}
|
||||
})
|
||||
processHandler.startNotify()
|
||||
return future
|
||||
}
|
||||
|
||||
private fun writeToStdinListener(text: String, charset: Charset): ProcessListener {
|
||||
return object : ProcessAdapter() {
|
||||
override fun startNotified(event: ProcessEvent) {
|
||||
try {
|
||||
val processInput = event.processHandler.processInput
|
||||
if (processInput == null) {
|
||||
return
|
||||
}
|
||||
processInput.write(text.toByteArray(charset))
|
||||
processInput.close()
|
||||
}
|
||||
catch (e: IOException) {
|
||||
LOG.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.black
|
||||
|
||||
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
|
||||
import com.intellij.execution.target.readableFs.PathInfo
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.fileTypes.FileTypeRegistry
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PythonFileType
|
||||
import com.jetbrains.python.packaging.PyPackage
|
||||
import com.jetbrains.python.packaging.PyPackageManager
|
||||
import com.jetbrains.python.pyi.PyiFileType
|
||||
import com.jetbrains.python.sdk.add.target.ValidationRequest
|
||||
import com.jetbrains.python.sdk.add.target.validateExecutableFile
|
||||
import org.jetbrains.annotations.SystemDependent
|
||||
import java.io.File
|
||||
|
||||
class BlackFormatterUtil {
|
||||
companion object {
|
||||
val LOG = thisLogger()
|
||||
|
||||
const val PACKAGE_NAME: String = "black"
|
||||
|
||||
fun isFileApplicable(vFile: VirtualFile): Boolean {
|
||||
return FileTypeRegistry.getInstance().isFileOfType(vFile, PythonFileType.INSTANCE)
|
||||
|| FileTypeRegistry.getInstance().isFileOfType(vFile, PyiFileType.INSTANCE)
|
||||
}
|
||||
|
||||
fun isBlackFormatterInstalledOnProjectSdk(sdk: Sdk?): Boolean {
|
||||
val packageManager = sdk?.let { PyPackageManager.getInstance(it) }
|
||||
return packageManager?.let {
|
||||
it.packages?.any { pyPackage -> pyPackage.name == PACKAGE_NAME }
|
||||
} ?: false
|
||||
}
|
||||
|
||||
fun getBlackFormatterPackageInfo(sdk: Sdk?): PyPackage? {
|
||||
val packageManager = sdk?.let { PyPackageManager.getInstance(it) }
|
||||
return packageManager?.let {
|
||||
it.refreshAndGetPackages(false).firstOrNull { pyPackage -> pyPackage.name == PACKAGE_NAME }
|
||||
}
|
||||
}
|
||||
|
||||
fun detectBlackExecutable(): File? {
|
||||
val name = when {
|
||||
SystemInfo.isWindows -> "black.exe"
|
||||
else -> "black"
|
||||
}
|
||||
return PathEnvironmentVariableUtil.findInPath(name)
|
||||
}
|
||||
|
||||
fun isBlackExecutableDetected(): Boolean = detectBlackExecutable() != null
|
||||
|
||||
fun validateBlackExecutable(path: @SystemDependent String?): ValidationInfo? {
|
||||
return validateExecutableFile(ValidationRequest(
|
||||
path = path,
|
||||
fieldIsEmpty = PyBundle.message("black.executable.not.found", if (SystemInfo.isWindows) 0 else 1),
|
||||
pathInfoProvider = PathInfo.localPathInfoProvider
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.black
|
||||
|
||||
import com.intellij.execution.process.CapturingProcessHandler
|
||||
import com.intellij.execution.target.TargetedCommandLineBuilder
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironment
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.progress.runBlockingMaybeCancellable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Version
|
||||
import com.intellij.util.VersionUtil
|
||||
import com.jetbrains.python.black.configuration.BlackFormatterConfiguration
|
||||
import com.jetbrains.python.packaging.PyPackage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
class BlackFormatterVersionService(private val project: Project) {
|
||||
companion object {
|
||||
|
||||
private val PATTERN = Pattern.compile("(?<=black, )(([\\d]+\\.[\\d]+\\.?[\\d]*).*)(?= \\(compiled: )")
|
||||
|
||||
val UNKNOWN_VERSION = Version(0, 0, 0)
|
||||
|
||||
private fun getInstance(project: Project): BlackFormatterVersionService = project.service()
|
||||
|
||||
fun getVersion(project: Project): Version = getInstance(project).getVersion()
|
||||
}
|
||||
|
||||
private var version: Version
|
||||
|
||||
private var configuration: BlackFormatterConfiguration
|
||||
|
||||
init {
|
||||
configuration = BlackFormatterConfiguration.getBlackConfiguration(project).copy()
|
||||
version = getVersionFromSdkOrBinary()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun getVersion(): Version {
|
||||
if (BlackFormatterConfiguration.getBlackConfiguration(project) != configuration) {
|
||||
configuration = BlackFormatterConfiguration.getBlackConfiguration(project).copy()
|
||||
version = getVersionFromSdkOrBinary()
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
private fun getVersionFromSdkOrBinary(): Version {
|
||||
return when (configuration.executionMode) {
|
||||
BlackFormatterConfiguration.ExecutionMode.BINARY -> {
|
||||
configuration.pathToExecutable?.let {
|
||||
getVersionForExecutable(it)
|
||||
} ?: UNKNOWN_VERSION
|
||||
}
|
||||
BlackFormatterConfiguration.ExecutionMode.PACKAGE -> {
|
||||
BlackFormatterUtil.getBlackFormatterPackageInfo(configuration.getSdk(project))?.let {
|
||||
getVersionForPackage(it)
|
||||
} ?: UNKNOWN_VERSION
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVersionForExecutable(pathToExecutable: String): Version {
|
||||
val targetEnvRequest = LocalTargetEnvironmentRequest()
|
||||
val targetEnvironment = LocalTargetEnvironment(LocalTargetEnvironmentRequest())
|
||||
|
||||
val commandLineBuilder = TargetedCommandLineBuilder(targetEnvRequest)
|
||||
commandLineBuilder.setExePath(pathToExecutable)
|
||||
commandLineBuilder.addParameters("--version")
|
||||
|
||||
val targetCMD = commandLineBuilder.build()
|
||||
|
||||
val process = targetEnvironment.createProcess(targetCMD)
|
||||
|
||||
return runBlockingMaybeCancellable {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
val processHandler = CapturingProcessHandler(process, targetCMD.charset, targetCMD.getCommandPresentation(targetEnvironment))
|
||||
val processOutput = processHandler.runProcess(5000, true).stdout
|
||||
return@withContext VersionUtil.parseVersion(processOutput, PATTERN) ?: UNKNOWN_VERSION
|
||||
}
|
||||
}.getOrDefault(UNKNOWN_VERSION)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVersionForPackage(pythonPackage: PyPackage): Version =
|
||||
parseVersionString(pythonPackage.version)
|
||||
|
||||
private fun parseVersionString(versionString: String): Version {
|
||||
|
||||
val parts = versionString
|
||||
.split(".", "b")
|
||||
.mapNotNull(String::toIntOrNull)
|
||||
.filter { it >= 0 }
|
||||
|
||||
if (parts.size < 3) {
|
||||
return UNKNOWN_VERSION
|
||||
}
|
||||
|
||||
return Version(parts[0], parts[1], parts[2])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.black
|
||||
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.jetbrains.python.psi.PyIndentUtil
|
||||
|
||||
sealed class BlackFormattingRequest {
|
||||
abstract val fragmentToFormat: String
|
||||
abstract val virtualFile: VirtualFile
|
||||
|
||||
class Fragment(val fragment: String, override val virtualFile: VirtualFile) : BlackFormattingRequest() {
|
||||
private val extractedIndent: String
|
||||
private val whitespaceBefore: String
|
||||
private val whitespaceAfter: String
|
||||
private val endsWithNewLine: Boolean
|
||||
|
||||
override val fragmentToFormat: String
|
||||
|
||||
init {
|
||||
val firstNotEmptyLine = fragment.lines().first { it.isNotBlank() }
|
||||
|
||||
extractedIndent = PyIndentUtil.getLineIndent(firstNotEmptyLine)
|
||||
whitespaceBefore = fragment.takeWhile { it.isWhitespace() }
|
||||
whitespaceAfter = fragment.takeLastWhile { it.isWhitespace() }
|
||||
endsWithNewLine = fragment.endsWith("\n")
|
||||
|
||||
fragmentToFormat = PyIndentUtil.removeCommonIndent(fragment, false)
|
||||
}
|
||||
|
||||
fun postProcessResponse(response: String): String {
|
||||
return buildString {
|
||||
val lines = response.trimEnd().lines()
|
||||
|
||||
if (!response.contains('\n')) {
|
||||
append(extractedIndent)
|
||||
append(response)
|
||||
if (endsWithNewLine) {
|
||||
append('\n')
|
||||
}
|
||||
return@buildString
|
||||
}
|
||||
|
||||
append(whitespaceBefore)
|
||||
append(lines.first())
|
||||
for (line in lines.listIterator(1)) {
|
||||
appendLine()
|
||||
append(line.prependIndent(extractedIndent))
|
||||
}
|
||||
append(whitespaceAfter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class File(override val fragmentToFormat: String, override val virtualFile: VirtualFile) : BlackFormattingRequest()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.black
|
||||
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import org.jetbrains.annotations.Nls
|
||||
|
||||
sealed class BlackFormattingResponse {
|
||||
class Success(val formattedText: String) : BlackFormattingResponse()
|
||||
|
||||
class Ignored(@Nls val title: String,
|
||||
@NlsSafe val description: String) : BlackFormattingResponse()
|
||||
|
||||
class Failure(@Nls val title: String,
|
||||
@NlsSafe val description: String,
|
||||
val exitCode: Int?) : BlackFormattingResponse() {
|
||||
|
||||
fun getLoggingMessage(): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
stringBuilder.append("${title}\n")
|
||||
if (description.isNotEmpty()) {
|
||||
stringBuilder.append("stderr: ${description}\n")
|
||||
}
|
||||
if (exitCode != null) {
|
||||
stringBuilder.append("exit code: ${exitCode}\n")
|
||||
}
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.black
|
||||
|
||||
import com.intellij.codeInsight.actions.VcsFacade
|
||||
import com.intellij.codeInsight.hint.HintManager
|
||||
import com.intellij.codeInsight.hint.HintManagerImpl
|
||||
import com.intellij.codeInsight.hint.HintUtil
|
||||
import com.intellij.formatting.service.AsyncDocumentFormattingService
|
||||
import com.intellij.formatting.service.AsyncFormattingRequest
|
||||
import com.intellij.formatting.service.FormattingService
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.ReadAction
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.util.PsiEditorUtil
|
||||
import com.intellij.ui.LightweightHint
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.black.configuration.BlackFormatterConfiguration
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.time.toKotlinDuration
|
||||
|
||||
|
||||
class BlackFormattingService : AsyncDocumentFormattingService() {
|
||||
companion object {
|
||||
private val LOG = thisLogger()
|
||||
val NAME: String = PyBundle.message("black.formatting.service.name")
|
||||
val DEFAULT_CHARSET: Charset = StandardCharsets.UTF_8
|
||||
const val NOTIFICATION_GROUP_ID = "Black Formatter Integration"
|
||||
val FEATURES: Set<FormattingService.Feature> = setOf(FormattingService.Feature.FORMAT_FRAGMENTS)
|
||||
}
|
||||
|
||||
override fun getFeatures(): Set<FormattingService.Feature> = FEATURES
|
||||
|
||||
override fun canFormat(source: PsiFile): Boolean {
|
||||
if (!Registry.`is`("black.formatter.support.enabled")) return false
|
||||
val project = source.project
|
||||
val blackConfiguration = BlackFormatterConfiguration.getBlackConfiguration(project)
|
||||
|
||||
BlackFormatterAdvertiserService.getInstance().suggestBlack(project, blackConfiguration)
|
||||
|
||||
if (!blackConfiguration.enabledOnReformat) return false
|
||||
val vFile = source.virtualFile ?: return false
|
||||
return BlackFormatterUtil.isFileApplicable(vFile)
|
||||
}
|
||||
|
||||
override fun createFormattingTask(formattingRequest: AsyncFormattingRequest): FormattingTask? {
|
||||
val formattingContext = formattingRequest.context
|
||||
val file = formattingContext.containingFile
|
||||
val vFile = formattingContext.virtualFile ?: return null
|
||||
val formattingRange = formattingRequest.formattingRanges[0]
|
||||
val project = formattingContext.project
|
||||
val blackConfig = BlackFormatterConfiguration.getBlackConfiguration(project)
|
||||
val sdk = blackConfig.getSdk(project)
|
||||
|
||||
if (sdk == null) {
|
||||
val message = PyBundle.message("black.sdk.not.configured.error", project.name)
|
||||
LOG.warn(message)
|
||||
formattingRequest.onError(PyBundle.message("black.sdk.not.configured.error.title"), message)
|
||||
return null
|
||||
}
|
||||
|
||||
val document = ReadAction.compute<Document?, RuntimeException> {
|
||||
PsiDocumentManager.getInstance(project).getDocument(file)
|
||||
}
|
||||
if (document == null) {
|
||||
LOG.warn("Document for file ${file.name} is null")
|
||||
return null
|
||||
}
|
||||
|
||||
val text = document.text
|
||||
val fragment: String = document.getText(formattingRange)
|
||||
if (fragment.isEmpty()) return null
|
||||
|
||||
val editor = PsiEditorUtil.findEditor(file)
|
||||
|
||||
val blackFormattingRequest = if (isFormatFragmentAction(document, formattingRange))
|
||||
BlackFormattingRequest.Fragment(fragment, vFile)
|
||||
else
|
||||
BlackFormattingRequest.File(fragment, vFile)
|
||||
|
||||
return object : FormattingTask {
|
||||
override fun run() {
|
||||
runCatching {
|
||||
val executor = BlackFormatterExecutor(project, sdk, blackConfig)
|
||||
|
||||
when (val response = executor.getBlackFormattingResponse(blackFormattingRequest, timeout.toKotlinDuration())) {
|
||||
is BlackFormattingResponse.Success -> {
|
||||
val formattedFragment = response.formattedText
|
||||
val formattedDocumentText = when (blackFormattingRequest) {
|
||||
is BlackFormattingRequest.Fragment -> {
|
||||
val postProcessedFragment = blackFormattingRequest.postProcessResponse(formattedFragment)
|
||||
text.replaceRange(IntRange(formattingRange.startOffset, formattingRange.endOffset - 1), postProcessedFragment)
|
||||
}
|
||||
is BlackFormattingRequest.File -> {
|
||||
formattedFragment
|
||||
}
|
||||
}
|
||||
formattingRequest.onTextReady(formattedDocumentText)
|
||||
val message = buildNotificationMessage(document, formattedDocumentText)
|
||||
showFormattedLinesInfo(editor, message)
|
||||
}
|
||||
is BlackFormattingResponse.Failure -> {
|
||||
LOG.warn(response.getLoggingMessage())
|
||||
formattingRequest.onError(response.title, response.description)
|
||||
}
|
||||
is BlackFormattingResponse.Ignored -> {
|
||||
showFormattedLinesInfo(editor, PyBundle.message("black.file.ignored.notification.message", vFile.name))
|
||||
}
|
||||
}
|
||||
}.onFailure { exception ->
|
||||
when (exception) {
|
||||
is ProcessCanceledException -> { /* ignore */ }
|
||||
else -> {
|
||||
LOG.warn(exception)
|
||||
formattingRequest.onError(PyBundle.message("black.exception.error.message"), exception.localizedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isRunUnderProgress(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotificationMessage(document: Document, textBefore: CharSequence): @Nls String {
|
||||
val diff = VcsFacade.getInstance().calculateChangedLinesNumber(document, textBefore)
|
||||
return if (diff == 0)
|
||||
PyBundle.message("black.no.lines.changed")
|
||||
else
|
||||
PyBundle.message("black.formatted.n.lines", diff, if (diff == 1) 1 else 0)
|
||||
}
|
||||
|
||||
private fun showFormattedLinesInfo(editor: Editor?, text: @Nls String) {
|
||||
if (editor != null) {
|
||||
ApplicationManager.getApplication()
|
||||
.invokeLater({
|
||||
val component = HintUtil.createInformationLabel(text, null, null, null)
|
||||
val hint = LightweightHint(component)
|
||||
HintManagerImpl.getInstanceImpl()
|
||||
.showEditorHint(hint, editor, HintManager.ABOVE,
|
||||
HintManager.HIDE_BY_ANY_KEY or HintManager.HIDE_BY_SCROLLING, 0,
|
||||
false)
|
||||
},
|
||||
ModalityState.defaultModalityState()) { editor.isDisposed || !editor.component.isShowing }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isFormatFragmentAction(document: Document, range: TextRange): Boolean =
|
||||
range.length != document.textLength
|
||||
|
||||
override fun getNotificationGroupId(): String = NOTIFICATION_GROUP_ID
|
||||
|
||||
override fun getName(): String = NAME
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.black.configuration
|
||||
|
||||
import com.intellij.codeInsight.AutoPopupController
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.actionsOnSave.*
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.IdeActions
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.editor.SpellCheckingEditorCustomizationProvider
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.keymap.KeymapUtil
|
||||
import com.intellij.openapi.options.BoundConfigurable
|
||||
import com.intellij.openapi.progress.runBlockingModal
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.modules
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.*
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.ui.EnumComboBoxModel
|
||||
import com.intellij.ui.SimpleListCellRenderer
|
||||
import com.intellij.ui.TextFieldWithAutoCompletionListProvider
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.intellij.util.io.await
|
||||
import com.intellij.util.text.nullize
|
||||
import com.intellij.util.textCompletion.TextCompletionUtil
|
||||
import com.intellij.util.textCompletion.TextFieldWithCompletion
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.webcore.packaging.PackageManagementService
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.black.BlackFormatterUtil
|
||||
import com.jetbrains.python.black.BlackFormatterVersionService
|
||||
import com.jetbrains.python.black.configuration.BlackFormatterConfiguration.BlackFormatterOption.Companion.toCliOptionFlags
|
||||
import com.jetbrains.python.newProject.steps.createPythonSdkComboBox
|
||||
import com.jetbrains.python.packaging.PyPackageManagers
|
||||
import com.jetbrains.python.packaging.PyPackagesNotificationPanel
|
||||
import com.jetbrains.python.sdk.PythonSdkAdditionalData
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.io.File
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JCheckBox
|
||||
import javax.swing.JLabel
|
||||
|
||||
const val CONFIGURABLE_ID = "com.jetbrains.python.black.configuration.BlackFormatterConfigurable"
|
||||
|
||||
class BlackFormatterConfigurable(val project: Project) : BoundConfigurable(PyBundle.message("black.configurable.name")) {
|
||||
|
||||
private var storedState: BlackFormatterConfiguration
|
||||
|
||||
private var isBlackFormatterPackageInstalled: Boolean = false
|
||||
private var detectedBlackExecutable: File? = null
|
||||
private var selectedSdk: Sdk? = null
|
||||
private var isLocalSdk = false
|
||||
|
||||
private lateinit var enableOnReformatCheckBox: JCheckBox
|
||||
private lateinit var enableOnSaveCheckBox: JCheckBox
|
||||
private lateinit var packageNotInstalledErrorLabel: JLabel
|
||||
private lateinit var remoteSdkErrorLabel: JLabel
|
||||
private lateinit var installButton: JButton
|
||||
private lateinit var installPanel: Panel
|
||||
private lateinit var settingsPanel: Panel
|
||||
private lateinit var pathToBinaryRow: Row
|
||||
private lateinit var sdkSelectionRow: Row
|
||||
private lateinit var cliArgumentsRow: Row
|
||||
private lateinit var executionModeComboBox: ComboBox<BlackFormatterConfiguration.ExecutionMode>
|
||||
|
||||
private val blackExecutablePathField = TextFieldWithBrowseButton().apply {
|
||||
addBrowseFolderListener(
|
||||
@Suppress("DialogTitleCapitalization")
|
||||
PyBundle.message("black.select.path.to.executable"),
|
||||
null,
|
||||
project,
|
||||
FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor()
|
||||
)
|
||||
}
|
||||
|
||||
private val sdkSelectionComboBox = createPythonSdkComboBox(project.modules.mapNotNull { it.pythonSdk }, null)
|
||||
|
||||
private val cliArgumentsTextField = BlackTextFieldWithAutoCompletion(project, object :
|
||||
TextFieldWithAutoCompletionListProvider<BlackFormatterConfiguration.CliOptionFlag>(
|
||||
BlackFormatterConfiguration.options.toCliOptionFlags()) {
|
||||
|
||||
override fun getLookupString(item: BlackFormatterConfiguration.CliOptionFlag): String = item.flag + " "
|
||||
|
||||
override fun getTailText(item: BlackFormatterConfiguration.CliOptionFlag): String? = item.option.param
|
||||
|
||||
override fun getTypeText(item: BlackFormatterConfiguration.CliOptionFlag): String = item.description()
|
||||
})
|
||||
|
||||
var mainPanel: DialogPanel = panel {
|
||||
row {
|
||||
label(PyBundle.message("black.execution.mode.label"))
|
||||
.applyToComponent { toolTipText = PyBundle.message("black.execution.mode.tooltip.text") }
|
||||
.applyToComponent { icon = AllIcons.General.ContextHelp }
|
||||
executionModeComboBox = comboBox(EnumComboBoxModel(
|
||||
BlackFormatterConfiguration.ExecutionMode::class.java))
|
||||
.applyToComponent { renderer = executionModeComboBoxRenderer }
|
||||
.component
|
||||
layout(RowLayout.LABEL_ALIGNED)
|
||||
}
|
||||
row {
|
||||
remoteSdkErrorLabel = label(PyBundle.message("black.remote.sdk.error"))
|
||||
.applyToComponent { icon = AllIcons.General.Warning }
|
||||
.visible(false)
|
||||
.component
|
||||
}
|
||||
pathToBinaryRow = row(PyBundle.message("black.executable.label")) {
|
||||
layout(RowLayout.LABEL_ALIGNED)
|
||||
cell(blackExecutablePathField)
|
||||
.validationInfo { blackExecutableValidationInfo() }
|
||||
.onChanged { updateUiState() }
|
||||
.align(AlignX.FILL)
|
||||
bottomGap(BottomGap.SMALL)
|
||||
}
|
||||
sdkSelectionRow = row(PyBundle.message("black.sdk.selection.combobox.label")) {
|
||||
layout(RowLayout.LABEL_ALIGNED)
|
||||
cell(sdkSelectionComboBox)
|
||||
.resizableColumn()
|
||||
.align(AlignX.FILL)
|
||||
.columns(COLUMNS_SHORT)
|
||||
}
|
||||
installPanel = panel {
|
||||
row {
|
||||
packageNotInstalledErrorLabel = label(PyBundle.message("black.not.installed.error"))
|
||||
.applyToComponent { icon = AllIcons.General.Warning }
|
||||
.component
|
||||
installButton = button(PyBundle.message("black.install.button.label")) {
|
||||
runBlockingModal(project, PyBundle.message("black.installing.modal.title")) {
|
||||
withContext(Dispatchers.EDT) {
|
||||
if (selectedSdk != null) {
|
||||
val errorDescription = installBlackFormatter(selectedSdk!!)
|
||||
if (errorDescription == null) {
|
||||
isBlackFormatterPackageInstalled = true
|
||||
enableOnReformatCheckBox.isSelected = true
|
||||
updateUiState()
|
||||
}
|
||||
else {
|
||||
PyPackagesNotificationPanel
|
||||
.showPackageInstallationError(@Suppress("DialogTitleCapitalization")
|
||||
PyBundle.message("black.installation.error.title"),
|
||||
errorDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.component
|
||||
}
|
||||
}
|
||||
settingsPanel = panel {
|
||||
row(PyBundle.message("black.use.section.label")) {
|
||||
layout(RowLayout.LABEL_ALIGNED)
|
||||
enableOnReformatCheckBox = checkBox(PyBundle.message("black.enable.black.checkbox.label")).component
|
||||
val shortcut = ActionManager.getInstance().getKeyboardShortcut(IdeActions.ACTION_EDITOR_REFORMAT)
|
||||
shortcut?.let { comment(KeymapUtil.getShortcutText(it)) }
|
||||
}
|
||||
row {
|
||||
label("")
|
||||
layout(RowLayout.LABEL_ALIGNED)
|
||||
bottomGap(BottomGap.SMALL)
|
||||
enableOnSaveCheckBox = checkBox(PyBundle.message("black.enable.action.on.save.label")).component
|
||||
val link = ActionsOnSaveConfigurable.createGoToActionsOnSavePageLink()
|
||||
cell(link)
|
||||
}
|
||||
cliArgumentsRow = row(PyBundle.message("black.cli.args.text.field.label")) {
|
||||
cell(cliArgumentsTextField)
|
||||
.resizableColumn()
|
||||
.align(AlignX.FILL)
|
||||
.applyToComponent {
|
||||
background = UIUtil.getTextFieldBackground()
|
||||
}
|
||||
.comment(PyBundle.message("black.cli.args.comment"), MAX_LINE_LENGTH_WORD_WRAP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
storedState = BlackFormatterConfiguration.getBlackConfiguration(project)
|
||||
|
||||
selectedSdk = storedState.getSdk(project)
|
||||
?: if (project.modules.size == 1) project.pythonSdk
|
||||
else null
|
||||
|
||||
updateSdkInfo()
|
||||
|
||||
detectedBlackExecutable = BlackFormatterUtil.detectBlackExecutable()
|
||||
|
||||
executionModeComboBox.addActionListener { updateUiState() }
|
||||
|
||||
sdkSelectionComboBox.addActionListener {
|
||||
selectedSdk = sdkSelectionComboBox.item
|
||||
updateSdkInfo()
|
||||
updateUiState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initForm() {
|
||||
enableOnReformatCheckBox.isSelected = storedState.enabledOnReformat
|
||||
enableOnSaveCheckBox.isSelected = storedState.enabledOnSave
|
||||
executionModeComboBox.item = storedState.executionMode
|
||||
cliArgumentsTextField.text = storedState.cmdArguments ?: ""
|
||||
sdkSelectionComboBox.item = selectedSdk
|
||||
|
||||
blackExecutablePathField.emptyText.text = getBlackExecPathPlaceholderMessage()
|
||||
storedState.pathToExecutable?.let {
|
||||
blackExecutablePathField.text = it
|
||||
}
|
||||
|
||||
if (storedState == BlackFormatterConfiguration()) {
|
||||
if (isBlackFormatterPackageInstalled) {
|
||||
executionModeComboBox.selectedItem = BlackFormatterConfiguration.ExecutionMode.PACKAGE
|
||||
}
|
||||
else if (detectedBlackExecutable != null) {
|
||||
executionModeComboBox.selectedItem = BlackFormatterConfiguration.ExecutionMode.BINARY
|
||||
}
|
||||
}
|
||||
|
||||
updateUiState()
|
||||
}
|
||||
|
||||
private fun updateUiState() {
|
||||
val isBinaryMode = executionModeComboBox.selectedItem == BlackFormatterConfiguration.ExecutionMode.BINARY
|
||||
|
||||
installPanel.visible(!isBlackFormatterPackageInstalled && !isBinaryMode && isLocalSdk)
|
||||
pathToBinaryRow.visible(isBinaryMode)
|
||||
sdkSelectionRow.visible(!isBinaryMode)
|
||||
|
||||
if (selectedSdk == null) {
|
||||
packageNotInstalledErrorLabel.text = PyBundle.message("black.no.project.interpreter.error")
|
||||
installButton.isVisible = false
|
||||
}
|
||||
else if (!isLocalSdk) {
|
||||
remoteSdkErrorLabel.isVisible = executionModeComboBox.selectedItem == BlackFormatterConfiguration.ExecutionMode.PACKAGE
|
||||
}
|
||||
|
||||
val canBeEnabled = canBeEnabled()
|
||||
settingsPanel.enabled(canBeEnabled)
|
||||
enableOnReformatCheckBox.isSelected = storedState.enabledOnReformat && canBeEnabled
|
||||
enableOnSaveCheckBox.isSelected = storedState.enabledOnSave && canBeEnabled
|
||||
}
|
||||
|
||||
private fun updateSdkInfo() {
|
||||
isLocalSdk = selectedSdk?.let { it.sdkType.isLocalSdk(it) } ?: false
|
||||
isBlackFormatterPackageInstalled = BlackFormatterUtil.isBlackFormatterInstalledOnProjectSdk(selectedSdk)
|
||||
}
|
||||
|
||||
private suspend fun installBlackFormatter(sdk: Sdk): PackageManagementService.ErrorDescription? {
|
||||
val manager = PyPackageManagers.getInstance().getManagementService(project, sdk)
|
||||
val blackPackage = manager.allPackagesCached.firstOrNull { pyPackage -> pyPackage.name == BlackFormatterUtil.PACKAGE_NAME }
|
||||
val result = CompletableFuture<PackageManagementService.ErrorDescription>()
|
||||
val listener = object : PackageManagementService.Listener {
|
||||
override fun operationStarted(packageName: String?) {}
|
||||
|
||||
override fun operationFinished(packageName: String?, errorDescription: PackageManagementService.ErrorDescription?) {
|
||||
if (errorDescription == null) {
|
||||
result.complete(null)
|
||||
}
|
||||
else {
|
||||
result.complete(errorDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
manager.installPackage(blackPackage, null, false, null, listener, false)
|
||||
return result.await()
|
||||
}
|
||||
|
||||
private fun canBeEnabled(): Boolean {
|
||||
return when (executionModeComboBox.selectedItem) {
|
||||
BlackFormatterConfiguration.ExecutionMode.BINARY ->
|
||||
detectedBlackExecutable != null || storedState.pathToExecutable != null || blackExecutableValidationInfo() == null
|
||||
BlackFormatterConfiguration.ExecutionMode.PACKAGE ->
|
||||
selectedSdk != null && isLocalSdk && isBlackFormatterPackageInstalled
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyToConfig(configuration: BlackFormatterConfiguration): BlackFormatterConfiguration = configuration.apply {
|
||||
executionMode = executionModeComboBox.item
|
||||
enabledOnReformat = enableOnReformatCheckBox.isSelected
|
||||
enabledOnSave = enableOnSaveCheckBox.isSelected
|
||||
cmdArguments = cliArgumentsTextField.text
|
||||
sdkUUID = (sdkSelectionComboBox.item?.sdkAdditionalData as? PythonSdkAdditionalData)?.uuid.toString()
|
||||
|
||||
pathToExecutable = if (blackExecutableValidationInfo() == null) {
|
||||
blackExecutablePathField.text.nullize() ?: BlackFormatterUtil.detectBlackExecutable()?.absolutePath
|
||||
}
|
||||
else null
|
||||
}
|
||||
|
||||
private fun blackExecutableValidationInfo(): ValidationInfo? =
|
||||
BlackFormatterUtil.validateBlackExecutable(
|
||||
blackExecutablePathField.text.nullize() ?: BlackFormatterUtil.detectBlackExecutable()?.absolutePath)
|
||||
|
||||
private fun getBlackExecPathPlaceholderMessage(): String {
|
||||
return BlackFormatterUtil.detectBlackExecutable()?.let {
|
||||
PyBundle.message("black.executable.auto.detected.path", it.absolutePath)
|
||||
} ?: PyBundle.message("black.executable.not.found", if (SystemInfo.isWindows) 0 else 1)
|
||||
}
|
||||
|
||||
override fun isModified(): Boolean = storedState != applyToConfig(storedState.copy())
|
||||
|
||||
override fun reset() {
|
||||
initForm()
|
||||
updateUiState()
|
||||
}
|
||||
|
||||
override fun apply() {
|
||||
applyToConfig(storedState)
|
||||
}
|
||||
override fun createPanel(): DialogPanel {
|
||||
mainPanel.registerValidators(disposable!!)
|
||||
return mainPanel
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val executionModeComboBoxRenderer =
|
||||
SimpleListCellRenderer.create<BlackFormatterConfiguration.ExecutionMode>(
|
||||
SimpleListCellRenderer.Customizer { label, value, _ ->
|
||||
val text: @Nls String = when (value) {
|
||||
BlackFormatterConfiguration.ExecutionMode.PACKAGE -> PyBundle.message("black.execution.mode.package")
|
||||
BlackFormatterConfiguration.ExecutionMode.BINARY -> PyBundle.message("black.execution.mode.binary")
|
||||
null -> ""
|
||||
}
|
||||
label.text = text
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
class BlackTextFieldWithAutoCompletion(project: Project,
|
||||
provider: TextFieldWithAutoCompletionListProvider<BlackFormatterConfiguration.CliOptionFlag>)
|
||||
: TextFieldWithCompletion(project, provider, "", true, true, true) {
|
||||
override fun createEditor(): EditorEx {
|
||||
val editor = super.createEditor()
|
||||
val disableSpellChecking = SpellCheckingEditorCustomizationProvider.getInstance().disabledCustomization
|
||||
disableSpellChecking?.customize(editor)
|
||||
editor.putUserData(AutoPopupController.ALWAYS_AUTO_POPUP, true)
|
||||
val completionShortcut = KeymapUtil.getFirstKeyboardShortcutText(
|
||||
ActionManager.getInstance().getAction(IdeActions.ACTION_CODE_COMPLETION))
|
||||
if (completionShortcut.isNotEmpty()) {
|
||||
TextCompletionUtil.installCompletionHint(editor)
|
||||
}
|
||||
return editor
|
||||
}
|
||||
|
||||
override fun getText(): String = super.getText().trimEnd()
|
||||
}
|
||||
|
||||
class BlackFormatterActionOnSaveInfoProvider : ActionOnSaveInfoProvider() {
|
||||
override fun getActionOnSaveInfos(context: ActionOnSaveContext):
|
||||
List<ActionOnSaveInfo> = listOf(BlackFormatterActionOnSaveInfo(context))
|
||||
|
||||
override fun getSearchableOptions(): Collection<String> {
|
||||
return listOf(PyBundle.message("black.action.on.save.name"), PyBundle.message("black.configurable.name"))
|
||||
}
|
||||
}
|
||||
|
||||
internal class BlackFormatterActionOnSaveInfo(actionOnSaveContext: ActionOnSaveContext)
|
||||
: ActionOnSaveBackedByOwnConfigurable<BlackFormatterConfigurable>(actionOnSaveContext, CONFIGURABLE_ID,
|
||||
BlackFormatterConfigurable::class.java) {
|
||||
|
||||
override fun setActionOnSaveEnabled(configurable: BlackFormatterConfigurable, enabled: Boolean) {
|
||||
configurable.enableOnSaveCheckBox.isSelected = enabled
|
||||
}
|
||||
|
||||
override fun getCommentAccordingToStoredState() =
|
||||
getCommentForBlack(BlackFormatterConfiguration.getBlackConfiguration(project))
|
||||
|
||||
override fun getCommentAccordingToUiState(configurable: BlackFormatterConfigurable) =
|
||||
getCommentForBlack(configurable.storedState)
|
||||
|
||||
private fun getCommentForBlack(configuration: BlackFormatterConfiguration): ActionOnSaveComment {
|
||||
val version = runBlockingModal(project, "") {
|
||||
BlackFormatterVersionService.getVersion(project)
|
||||
}
|
||||
return when (configuration.executionMode) {
|
||||
BlackFormatterConfiguration.ExecutionMode.BINARY -> {
|
||||
configuration.pathToExecutable?.let {
|
||||
ActionOnSaveComment.info(PyBundle.message("black.action.on.save.executable.info", version, it))
|
||||
} ?: ActionOnSaveComment.warning(PyBundle.message("black.action.on.save.executable.path.not.specified"))
|
||||
}
|
||||
BlackFormatterConfiguration.ExecutionMode.PACKAGE -> {
|
||||
if (version != BlackFormatterVersionService.UNKNOWN_VERSION) {
|
||||
ActionOnSaveComment.info(PyBundle.message("black.action.on.save.package.info", version))
|
||||
}
|
||||
else {
|
||||
ActionOnSaveComment.warning(PyBundle.message("black.not.installed.error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isActionOnSaveEnabledAccordingToUiState(configurable: BlackFormatterConfigurable): Boolean {
|
||||
return configurable.enableOnSaveCheckBox.isSelected
|
||||
}
|
||||
|
||||
override fun isActionOnSaveEnabledAccordingToStoredState(): Boolean =
|
||||
BlackFormatterConfiguration.getBlackConfiguration(project).enabledOnSave
|
||||
|
||||
override fun getActionOnSaveName(): String = PyBundle.message("black.action.on.save.name")
|
||||
|
||||
override fun isApplicableAccordingToUiState(configurable: BlackFormatterConfigurable) =
|
||||
when (configurable.storedState.executionMode) {
|
||||
BlackFormatterConfiguration.ExecutionMode.PACKAGE ->
|
||||
BlackFormatterUtil.isBlackFormatterInstalledOnProjectSdk(configurable.selectedSdk)
|
||||
BlackFormatterConfiguration.ExecutionMode.BINARY ->
|
||||
BlackFormatterUtil.isBlackExecutableDetected()
|
||||
}
|
||||
|
||||
override fun isApplicableAccordingToStoredState(): Boolean {
|
||||
val configuration = BlackFormatterConfiguration.getBlackConfiguration(project)
|
||||
return when (configuration.executionMode) {
|
||||
BlackFormatterConfiguration.ExecutionMode.PACKAGE ->
|
||||
BlackFormatterUtil.isBlackFormatterInstalledOnProjectSdk(configuration.getSdk(project))
|
||||
BlackFormatterConfiguration.ExecutionMode.BINARY ->
|
||||
BlackFormatterUtil.isBlackExecutableDetected()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getActionLinks() = listOf(createGoToPageInSettingsLink(CONFIGURABLE_ID))
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.black.configuration
|
||||
|
||||
import com.intellij.openapi.options.ConfigurableProvider
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
|
||||
class BlackFormatterConfigurableProvider(val project: Project) : ConfigurableProvider() {
|
||||
|
||||
override fun canCreateConfigurable(): Boolean {
|
||||
return Registry.`is`("black.formatter.support.enabled")
|
||||
}
|
||||
|
||||
override fun createConfigurable(): BlackFormatterConfigurable {
|
||||
return BlackFormatterConfigurable(project)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.black.configuration
|
||||
|
||||
import com.intellij.openapi.components.PersistentStateComponent
|
||||
import com.intellij.openapi.components.State
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.modules
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.util.xmlb.XmlSerializerUtil
|
||||
import com.jetbrains.python.sdk.PythonSdkAdditionalData
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
import java.util.*
|
||||
|
||||
const val BLACK_ID: String = "Black"
|
||||
|
||||
@State(name = BLACK_ID)
|
||||
data class BlackFormatterConfiguration(var enabledOnReformat: Boolean,
|
||||
var enabledOnSave: Boolean,
|
||||
var executionMode: ExecutionMode,
|
||||
var pathToExecutable: String?,
|
||||
var cmdArguments: String,
|
||||
var sdkUUID: String?)
|
||||
: PersistentStateComponent<BlackFormatterConfiguration> {
|
||||
|
||||
@Suppress("unused") // Empty constructor required for state components
|
||||
constructor() : this(false,
|
||||
false,
|
||||
ExecutionMode.PACKAGE,
|
||||
null,
|
||||
"",
|
||||
null)
|
||||
|
||||
enum class ExecutionMode {
|
||||
BINARY,
|
||||
PACKAGE,
|
||||
}
|
||||
|
||||
fun getSdk(project: Project): Sdk? = sdkUUID?.let { uuidString ->
|
||||
val uuid = UUID.fromString(uuidString)
|
||||
project.modules
|
||||
.mapNotNull { it.pythonSdk }
|
||||
.firstOrNull { sdk -> (sdk.sdkAdditionalData as PythonSdkAdditionalData).uuid == uuid }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val options = listOf(
|
||||
BlackFormatterOption(listOf("-l", "--line-length"), "<length>", "How many characters per line to allow. [default: 88]"),
|
||||
BlackFormatterOption(listOf("-x", "--skip-source-first-line"), null, "Skip the first line of the source code"),
|
||||
BlackFormatterOption(listOf("-S", "--skip-string-normalization"), null, "Don't normalize string quotes or prefixes"),
|
||||
BlackFormatterOption(listOf("-C", "--skip-magic-trailing-comma"), null, "Don't use trailing commas as a reason to split lines"),
|
||||
BlackFormatterOption(listOf("--fast", "--safe"), null, "Skip temporary sanity checks [default: --safe]"),
|
||||
BlackFormatterOption(listOf("--config"), "FILE", "Read configuration from FILE path."),
|
||||
BlackFormatterOption(listOf("--preview"), null, "Enable potentially disruptive style changes\n" +
|
||||
"that may be added to Black's main\n" +
|
||||
"functionality in the next major release."),
|
||||
BlackFormatterOption(listOf("-t", "--target-version"), "[ver1, ver2..]", "Python versions that should be supported by\n" +
|
||||
"Black's output. By default, Black will try\n" +
|
||||
"to infer this from the project metadata in\n" +
|
||||
"pyproject.toml. If this does not yield\n" +
|
||||
"conclusive results, Black will use per-file\n" +
|
||||
"auto-detection."),
|
||||
)
|
||||
|
||||
fun getBlackConfiguration(project: Project): BlackFormatterConfiguration = project.getService(BlackFormatterConfiguration::class.java)
|
||||
}
|
||||
|
||||
class CliOptionFlag(val flag: String, val option: BlackFormatterOption) {
|
||||
internal fun description(): String {
|
||||
if (isPrimaryFlag(flag)) {
|
||||
return option.description
|
||||
}
|
||||
val primaryFlag = option.flags.find(::isPrimaryFlag)
|
||||
return if (primaryFlag != null) "See $primaryFlag" else option.description
|
||||
}
|
||||
|
||||
private fun isPrimaryFlag(flag: String): Boolean = flag.startsWith("--")
|
||||
}
|
||||
|
||||
data class BlackFormatterOption(val flags: List<String>, val param: String?, val description: String) {
|
||||
|
||||
companion object {
|
||||
fun List<BlackFormatterOption>.toCliOptionFlags() = this.flatMap { option ->
|
||||
option.flags.map { CliOptionFlag(it, option) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getState(): BlackFormatterConfiguration = this
|
||||
|
||||
override fun loadState(state: BlackFormatterConfiguration) = XmlSerializerUtil.copyBean(state, this)
|
||||
}
|
||||
+5
-1
@@ -188,7 +188,11 @@ public abstract class PyEnvTestCase {
|
||||
final EnvTestTagsRequired classAnnotation = getClass().getAnnotation(EnvTestTagsRequired.class);
|
||||
EnvTestTagsRequired methodAnnotation = null;
|
||||
try {
|
||||
final Method method = getClass().getMethod(myTestName.getMethodName());
|
||||
String methodName = myTestName.getMethodName();
|
||||
if (methodName.contains("[")) {
|
||||
methodName = methodName.substring(0, methodName.indexOf('['));
|
||||
}
|
||||
final Method method = getClass().getMethod(methodName);
|
||||
methodAnnotation = method.getAnnotation(EnvTestTagsRequired.class);
|
||||
}
|
||||
catch (final NoSuchMethodException e) {
|
||||
|
||||
Reference in New Issue
Block a user