diff --git a/python/ide/impl/resources/intellij.pycharm.community.ide.impl.xml b/python/ide/impl/resources/intellij.pycharm.community.ide.impl.xml
index 1acdbacd313e..bed3c3844f76 100644
--- a/python/ide/impl/resources/intellij.pycharm.community.ide.impl.xml
+++ b/python/ide/impl/resources/intellij.pycharm.community.ide.impl.xml
@@ -71,6 +71,13 @@
displayNameKey="settings.python.group.title"
descriptionKey="settings.python.group.description"/>
+
+
{
- ep.groupId = PythonMainConfigurable.ID
+ ep.groupId = PythonToolsConfigurable.ID
ep.key = "configurable.PyIntegratedToolsModulesConfigurable.pycharm.display.name"
ep.bundle="messages.PyBundle"
ep.groupWeight = 20
}
+ "com.jetbrains.python.black.configuration.BlackFormatterConfigurable" -> {
+ ep.groupId = PythonToolsConfigurable.ID
+ ep.groupWeight = 30
+ }
}
}
}
diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/settings/PythonToolsConfigurable.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/settings/PythonToolsConfigurable.kt
new file mode 100644
index 000000000000..8cc39e9c20ae
--- /dev/null
+++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/settings/PythonToolsConfigurable.kt
@@ -0,0 +1,6 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.pycharm.community.ide.impl.settings
+
+internal object PythonToolsConfigurable {
+ const val ID: String = "python.tools.group.settings"
+}
\ No newline at end of file
diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties
index 62ee230f6b9b..9e2a8ed41015 100644
--- a/python/pluginResources/messages/PyBundle.properties
+++ b/python/pluginResources/messages/PyBundle.properties
@@ -225,6 +225,10 @@ settings.memory.indicator.row.comment=Displays current memory usage in the statu
settings.python.group.title=Python
settings.python.group.description=Configure key Python development settings to customize your coding, debugging, data visualization, and documentation workflows.
+# Python Tools Settings Group
+settings.python.tools.group.title=Tools
+settings.python.tools.group.description=Configure external Python tool settings.
+
runcfg.autodetect.display_name=Autodetect
runcfg.autodetect.parameters.group.name=Python test
@@ -1517,7 +1521,7 @@ black.executable.not.found=Unable to auto-detect Black executable in {0, choice,
black.execution.mode.package=Package
black.execution.mode.binary=Binary
black.execution.mode.tooltip.text=Use either executable or package installed on the project interpreter
-black.cli.args.text.field.label=Settings:
+black.cli.args.text.field.label=Arguments:
black.cli.args.comment=List command line arguments separated by whitespace. Learn more
\
Settings from pyproject.toml are applied automatically for Black v21.4.0 and higher.
black.cli.args.validation.notification=No value passed for parameter {0}
@@ -1530,6 +1534,30 @@ black.format.fragments.supported.info=Fragment formatting is supported for Black
black.format.fragment.fallback.title=Format Fragment
black.getting.black.version=Getting Black version\u2026
+# Python LSP Tools
+configurable.name=Python Language Services
+not.installed.error=Package is not installed on the current interpreter
+remote.sdk.error=Package mode is not available for remote SDKs. Please use Binary mode.
+install.button.label=Install
+use.section.label=Use:
+enable.checkbox.label=Enable
+enable.on.save.label=On save
+advanced.settings.panel.title=Advanced settings
+execution.mode.label=Execution mode:
+executable.label=Executable:
+executable.auto.detected.path=Auto-detected: {0}
+executable.not.found=Unable to auto-detect executable in {0, choice, 0#%PATH%|1#$PATH}
+execution.mode.package=Interpreter
+execution.mode.binary=Path
+cli.args.text.field.label=Arguments:
+cli.args.comment=List command line arguments separated by whitespace.
+cli.args.validation.notification=No value passed for parameter {0}
+sdk.not.configured.error=No project SDK configured for the project {0}
+sdk.not.configured.error.title=SDK not configured
+remote.sdk.exception.text=Invocation in Package mode is not allowed on remote SDKs
+getting.version=Getting version\u2026
+select.path.to.executable=Select Path to Executable
+
inlay.parameters.python.show.class.constructor.call.parameter.names=Class constructor calls
inlay.parameters.python.show.hints.for.non-literal.arguments=Non-literal arguments
inlay.parameters.python.hints.blacklist.explanation=\
@@ -1710,6 +1738,14 @@ pycharm.free.mode.upgrade.button=Purchase Subscription
python.packaging.sync.packages=Syncing project dependencies
python.sdk.read.only=Python SDK ''{0}'' is read only and operation cannot be performed.
read.only.python.sdk.system.wide.read.only.message=Modify packages of System Python is forbidden. To modify use another interpreter.
+button.install=Install {0}
+label.features=Features:
+checkbox.inspections=Inspections
+checkbox.completions=Completions
+checkbox.inlay.hints=Inlay hints
+label.features=Features:
+label.package.not.installed.in.current.interpreter={0} package is not installed in the current interpreter
+modal.progress.title.installing=Installing {0}\u2026
tracecontext.detecting.poetry.executable=Detecting Poetry Executable
tracecontext.detecting.pip.executable=Detecting Pip Executable
tracecontext.detecting.uv.executable=Detecting uv Executable
diff --git a/python/python-psi-impl/resources/intellij.python.psi.impl.xml b/python/python-psi-impl/resources/intellij.python.psi.impl.xml
index 45f3522215cb..5cb9b01c2deb 100644
--- a/python/python-psi-impl/resources/intellij.python.psi.impl.xml
+++ b/python/python-psi-impl/resources/intellij.python.psi.impl.xml
@@ -551,6 +551,8 @@
description="Enables incremental reparse for statement lists"/>
+
diff --git a/python/python-psi-impl/src/com/jetbrains/python/sdk/PySdkExtensions.kt b/python/python-psi-impl/src/com/jetbrains/python/sdk/PySdkExtensions.kt
index 58b8bf1043b0..7f7834a6c8f1 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/sdk/PySdkExtensions.kt
+++ b/python/python-psi-impl/src/com/jetbrains/python/sdk/PySdkExtensions.kt
@@ -3,8 +3,11 @@ package com.jetbrains.python.sdk
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.psi.PsiDocumentManager
+import com.intellij.util.system.OS
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
+import java.nio.file.Path
+import kotlin.io.path.Path
val Editor.pythonSdk: Sdk?
get() {
@@ -14,3 +17,15 @@ val Editor.pythonSdk: Sdk?
val pyPsiFile = viewProvider.allFiles.firstOrNull { it is PyFile } ?: return null
return PythonSdkUtil.findPythonSdk(pyPsiFile)
}
+
+fun Sdk.getExecutablePath(name: String): Path? = homePath?.let {
+ val base = Path(it)
+ if (OS.CURRENT == OS.Windows) {
+ val candidates = listOf("exe", "bat", "cmd", "com").map { ext -> Path(it + "." + ext) }
+ val found = candidates.firstNotNullOfOrNull { candidate -> PythonSdkUtil.getExecutablePath(candidate, name) }
+ found ?: PythonSdkUtil.getExecutablePath(base, name)
+ }
+ else {
+ PythonSdkUtil.getExecutablePath(base, name)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/black/BlackFormatterUtil.kt b/python/src/com/jetbrains/python/black/BlackFormatterUtil.kt
index 0420948ab77b..abf477e6aecb 100644
--- a/python/src/com/jetbrains/python/black/BlackFormatterUtil.kt
+++ b/python/src/com/jetbrains/python/black/BlackFormatterUtil.kt
@@ -19,7 +19,7 @@ import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateExecutableFile
import com.jetbrains.python.pyi.PyiFileType
import org.jetbrains.annotations.SystemDependent
-import java.io.File
+import java.nio.file.Path
class BlackFormatterUtil {
companion object {
@@ -38,12 +38,12 @@ class BlackFormatterUtil {
return packageManager?.hasInstalledPackageSnapshot(PACKAGE_NAME) ?: false
}
- fun detectBlackExecutable(): File? {
+ fun detectBlackExecutable(): Path? {
val name = when {
SystemInfo.isWindows -> "black.exe"
else -> "black"
}
- return PathEnvironmentVariableUtil.findInPath(name)
+ return PathEnvironmentVariableUtil.findInPath(name)?.toPath()
}
fun isBlackExecutableDetected(): Boolean = detectBlackExecutable() != null
diff --git a/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt b/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt
index b6ca85fd89a2..e51430348d8c 100644
--- a/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt
+++ b/python/src/com/jetbrains/python/black/configuration/BlackFormatterConfigurable.kt
@@ -32,16 +32,18 @@ import com.jetbrains.python.black.BlackFormatterUtil
import com.jetbrains.python.black.BlackFormatterVersionService
import com.jetbrains.python.black.BlackFormatterVersionService.Companion.UNKNOWN_VERSION
import com.jetbrains.python.black.configuration.BlackFormatterConfiguration.BlackFormatterOption.Companion.toCliOptionFlags
+import com.jetbrains.python.externaltools.configuration.createPythonSdkComboBox
import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI
import com.jetbrains.python.packaging.management.ui.installPackageBackground
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.nio.file.Path
import javax.swing.JButton
import javax.swing.JCheckBox
import javax.swing.JLabel
+import kotlin.io.path.absolutePathString
const val CONFIGURABLE_ID: String = "com.jetbrains.python.black.configuration.BlackFormatterConfigurable"
@@ -49,7 +51,7 @@ class BlackFormatterConfigurable(val project: Project) : BoundConfigurable(PyBun
private var storedState = BlackFormatterConfiguration.getBlackConfiguration(project)
private var isBlackFormatterPackageInstalled: Boolean = false
- private var detectedBlackExecutable: File? = null
+ private var detectedBlackExecutable: Path? = null
private var selectedSdk: Sdk? = null
private var isLocalSdk = false
@@ -71,7 +73,8 @@ class BlackFormatterConfigurable(val project: Project) : BoundConfigurable(PyBun
.withTitle(@Suppress("DialogTitleCapitalization") PyBundle.message("black.select.path.to.executable")))
}
- private val sdkSelectionComboBox = createPythonSdkComboBox(project.modules.mapNotNull { it.pythonSdk }, null)
+ // TODO: initial should be project default
+ private val sdkSelectionComboBox = createPythonSdkComboBox(project, null)
private val cliArgumentsTextField = BlackTextFieldWithAutoCompletion(project, object :
TextFieldWithAutoCompletionListProvider(
@@ -91,8 +94,7 @@ class BlackFormatterConfigurable(val project: Project) : BoundConfigurable(PyBun
.applyToComponent { renderer = executionModeComboBoxRenderer }
.gap(RightGap.SMALL)
.component
- icon(AllIcons.General.ContextHelp)
- .applyToComponent { toolTipText = PyBundle.message("black.execution.mode.tooltip.text") }
+ contextHelp(PyBundle.message("black.execution.mode.tooltip.text"))
layout(RowLayout.LABEL_ALIGNED)
}
row {
@@ -179,7 +181,6 @@ class BlackFormatterConfigurable(val project: Project) : BoundConfigurable(PyBun
executionModeComboBox.addActionListener { updateUiState() }
sdkSelectionComboBox.addActionListener {
- selectedSdk = sdkSelectionComboBox.item
updateSdkInfo()
updateUiState()
}
@@ -189,8 +190,7 @@ class BlackFormatterConfigurable(val project: Project) : BoundConfigurable(PyBun
enableOnReformatCheckBox.isSelected = storedState.enabledOnReformat
enableOnSaveCheckBox.isSelected = storedState.enabledOnSave
executionModeComboBox.item = storedState.executionMode
- cliArgumentsTextField.text = storedState.cmdArguments ?: ""
- sdkSelectionComboBox.item = selectedSdk
+ cliArgumentsTextField.text = storedState.cmdArguments
blackExecutablePathField.emptyText.text = getBlackExecPathPlaceholderMessage()
storedState.pathToExecutable?.let {
@@ -264,18 +264,18 @@ class BlackFormatterConfigurable(val project: Project) : BoundConfigurable(PyBun
sdkName = selectedSdk?.name
pathToExecutable = if (blackExecutableValidationInfo() == null) {
- blackExecutablePathField.text.nullize() ?: BlackFormatterUtil.detectBlackExecutable()?.absolutePath
+ blackExecutablePathField.text.nullize() ?: BlackFormatterUtil.detectBlackExecutable()?.absolutePathString()
}
else null
}
private fun blackExecutableValidationInfo(): ValidationInfo? =
BlackFormatterUtil.validateBlackExecutable(
- blackExecutablePathField.text.nullize() ?: BlackFormatterUtil.detectBlackExecutable()?.absolutePath)
+ blackExecutablePathField.text.nullize() ?: BlackFormatterUtil.detectBlackExecutable()?.absolutePathString())
private fun getBlackExecPathPlaceholderMessage(): String {
return BlackFormatterUtil.detectBlackExecutable()?.let {
- PyBundle.message("black.executable.auto.detected.path", it.absolutePath)
+ PyBundle.message("black.executable.auto.detected.path", it.absolutePathString())
} ?: PyBundle.message("black.executable.not.found", if (SystemInfo.isWindows) 0 else 1)
}
@@ -309,9 +309,10 @@ class BlackFormatterConfigurable(val project: Project) : BoundConfigurable(PyBun
}
- class BlackTextFieldWithAutoCompletion(project: Project,
- provider: TextFieldWithAutoCompletionListProvider)
- : TextFieldWithCompletion(project, provider, "", true, true, true) {
+ class BlackTextFieldWithAutoCompletion(
+ project: Project,
+ provider: TextFieldWithAutoCompletionListProvider,
+ ) : TextFieldWithCompletion(project, provider, "", true, true, true) {
override fun createEditor(): EditorEx {
val editor = super.createEditor()
val disableSpellChecking = SpellCheckingEditorCustomizationProvider.getInstance().disabledCustomization
@@ -353,7 +354,7 @@ class BlackFormatterConfigurable(val project: Project) : BoundConfigurable(PyBun
private fun getCommentForBlack(configuration: BlackFormatterConfiguration): ActionOnSaveComment {
val version = runWithModalProgressBlocking(project, PyBundle.message("black.getting.black.version")) {
- BlackFormatterVersionService.getVersion (project)
+ BlackFormatterVersionService.getVersion(project)
}
return when (configuration.executionMode) {
diff --git a/python/src/com/jetbrains/python/black/configuration/PySdkChooser.kt b/python/src/com/jetbrains/python/black/configuration/PySdkChooser.kt
deleted file mode 100644
index 6f7b6af0def4..000000000000
--- a/python/src/com/jetbrains/python/black/configuration/PySdkChooser.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright 2000-2022 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.projectRoots.Sdk
-import com.intellij.openapi.ui.ComboBox
-import com.intellij.openapi.util.io.FileUtil
-import com.intellij.ui.CollectionComboBoxModel
-import com.intellij.ui.ComboboxSpeedSearch
-import com.jetbrains.python.sdk.PySdkListCellRenderer
-
-internal fun createPythonSdkComboBox(sdks: List, initialSelection: Sdk?): ComboBox {
- val comboBox = ComboBox()
- comboBox.model = CollectionComboBoxModel(sdks.toMutableList(), initialSelection)
- comboBox.renderer = PySdkListCellRenderer()
- comboBox.addActionListener { comboBox.updateTooltip() }
- ComboboxSpeedSearch.installOn(comboBox)
- comboBox.updateTooltip()
- return comboBox
-}
-
-private fun ComboBox<*>.updateTooltip() {
- val item: Any? = getSelectedItem()
- val sdkHomePath = if (item is Sdk) item.homePath else null
- setToolTipText(if (sdkHomePath != null) FileUtil.toSystemDependentName(sdkHomePath) else null)
-}
-
diff --git a/python/src/com/jetbrains/python/externaltools/PyLspTool.kt b/python/src/com/jetbrains/python/externaltools/PyLspTool.kt
new file mode 100644
index 000000000000..220dca123995
--- /dev/null
+++ b/python/src/com/jetbrains/python/externaltools/PyLspTool.kt
@@ -0,0 +1,14 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.externaltools
+
+import com.intellij.openapi.util.NlsSafe
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Internal
+enum class PyLspTool(@NlsSafe val presentableName: String, val packageName: String) {
+ BASEDPYRIGHT("Basedpyright", "basedpyright"),
+ PYREFLY("Pyrefly", "pyrefly"),
+ PYRIGHT("Pyright", "pyright"),
+ RUFF("Ruff", "ruff"),
+ TY("ty", "ty"),
+}
diff --git a/python/src/com/jetbrains/python/externaltools/configuration/PyLspToolConfiguration.kt b/python/src/com/jetbrains/python/externaltools/configuration/PyLspToolConfiguration.kt
new file mode 100644
index 000000000000..d692a40b203d
--- /dev/null
+++ b/python/src/com/jetbrains/python/externaltools/configuration/PyLspToolConfiguration.kt
@@ -0,0 +1,34 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.externaltools.configuration
+
+import com.intellij.openapi.components.PersistentStateComponent
+import com.intellij.util.xmlb.XmlSerializerUtil
+import java.nio.file.Path
+import kotlin.io.path.Path
+
+abstract class PyLspToolConfiguration> : PersistentStateComponent {
+ var enabled: Boolean = false
+ var inspections: Boolean = true
+ open var completions: Boolean? = null
+
+ /**
+ * `null` means: not supported
+ */
+ open var inlayHints: Boolean? = null
+ var executableDiscoveryMode: ExecutableDiscoveryMode = ExecutableDiscoveryMode.INTERPRETER
+ var pathToExecutable: String? = null
+ val executablePath: Path? = pathToExecutable?.let { Path(it) }
+ var sdkName: String = DEFAULT_ENVIRONMENT
+
+ final override fun getState(): State = this as State
+
+ override fun loadState(state: State): Unit = XmlSerializerUtil.copyBean(state, this as State)
+}
+
+/**
+ * Executable discovery mode for LSP tools.
+ */
+enum class ExecutableDiscoveryMode {
+ INTERPRETER,
+ PATH,
+}
diff --git a/python/src/com/jetbrains/python/externaltools/configuration/PySdkChooser.kt b/python/src/com/jetbrains/python/externaltools/configuration/PySdkChooser.kt
new file mode 100644
index 000000000000..3d395a655beb
--- /dev/null
+++ b/python/src/com/jetbrains/python/externaltools/configuration/PySdkChooser.kt
@@ -0,0 +1,42 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.externaltools.configuration
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.modules
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.ui.ComboBox
+import com.intellij.openapi.util.io.FileUtil
+import com.intellij.ui.CollectionComboBoxModel
+import com.intellij.ui.ComboboxSpeedSearch
+import com.jetbrains.python.sdk.PySdkListCellRenderer
+import com.jetbrains.python.sdk.PythonSdkUtil
+import com.jetbrains.python.sdk.pythonSdk
+
+const val DEFAULT_ENVIRONMENT: String = "Project Default"
+
+fun Project.sdkByNameDefaultAware(name: String): Sdk? =
+ if (name == DEFAULT_ENVIRONMENT) pythonSdk else PythonSdkUtil.findSdkByKey(name)
+
+fun createPythonSdkComboBox(project: Project, initialSelection: Any?): ComboBox {
+ val sdks = buildList {
+ project.pythonSdk?.let { add(it) }
+ project.modules.mapNotNullTo(this) { it.pythonSdk }
+ }.distinct()
+ return ComboBox().apply {
+ model = CollectionComboBoxModel(
+ // we use a simple string here, ideally we should introduce some special class for this
+ (listOf(DEFAULT_ENVIRONMENT) + sdks).toMutableList(),
+ initialSelection,
+ )
+ renderer = PySdkListCellRenderer()
+ addActionListener { updateTooltip() }
+ ComboboxSpeedSearch.installOn(this)
+ updateTooltip()
+ }
+}
+
+private fun ComboBox<*>.updateTooltip() {
+ val item = selectedItem
+ val sdkHomePath = if (item is Sdk) item.homePath else null
+ setToolTipText(if (sdkHomePath != null) FileUtil.toSystemDependentName(sdkHomePath) else null)
+}