From 4f08c28e9dd04c13c0ac59c27862f5900a02fd59 Mon Sep 17 00:00:00 2001 From: Morgan Bartholomew Date: Fri, 29 Aug 2025 15:50:15 +1000 Subject: [PATCH] [python] python-lsp-core GitOrigin-RevId: e07d457d959a1b589861180c1be06b995218aa2c --- .../intellij.pycharm.community.ide.impl.xml | 7 ++++ ...CharmProjectConfigurableStartupActivity.kt | 7 +++- .../impl/settings/PythonToolsConfigurable.kt | 6 +++ .../messages/PyBundle.properties | 38 ++++++++++++++++- .../resources/intellij.python.psi.impl.xml | 2 + .../jetbrains/python/sdk/PySdkExtensions.kt | 15 +++++++ .../python/black/BlackFormatterUtil.kt | 6 +-- .../BlackFormatterConfigurable.kt | 31 +++++++------- .../black/configuration/PySdkChooser.kt | 26 ------------ .../python/externaltools/PyLspTool.kt | 14 +++++++ .../configuration/PyLspToolConfiguration.kt | 34 +++++++++++++++ .../configuration/PySdkChooser.kt | 42 +++++++++++++++++++ 12 files changed, 182 insertions(+), 46 deletions(-) create mode 100644 python/ide/impl/src/com/intellij/pycharm/community/ide/impl/settings/PythonToolsConfigurable.kt delete mode 100644 python/src/com/jetbrains/python/black/configuration/PySdkChooser.kt create mode 100644 python/src/com/jetbrains/python/externaltools/PyLspTool.kt create mode 100644 python/src/com/jetbrains/python/externaltools/configuration/PyLspToolConfiguration.kt create mode 100644 python/src/com/jetbrains/python/externaltools/configuration/PySdkChooser.kt 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) +}