[python] python-lsp-core

GitOrigin-RevId: e07d457d959a1b589861180c1be06b995218aa2c
This commit is contained in:
Morgan Bartholomew
2025-10-15 03:19:47 +00:00
committed by intellij-monorepo-bot
parent d9f3979bbe
commit 4f08c28e9d
12 changed files with 182 additions and 46 deletions
@@ -71,6 +71,13 @@
displayNameKey="settings.python.group.title"
descriptionKey="settings.python.group.description"/>
<groupConfigurable id="python.tools.group.settings"
parentId="python.group.settings"
weight="72"
bundle="messages.PyBundle"
displayNameKey="settings.python.tools.group.title"
descriptionKey="settings.python.tools.group.description"/>
<projectConfigurable groupId="python.group.settings"
id="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable"
bundle="messages.PyBundle"
@@ -5,6 +5,7 @@ import com.intellij.openapi.options.Configurable
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.pycharm.community.ide.impl.settings.PythonMainConfigurable
import com.intellij.pycharm.community.ide.impl.settings.PythonToolsConfigurable
class PyCharmProjectConfigurableStartupActivity : ProjectActivity {
@@ -37,11 +38,15 @@ class PyCharmProjectConfigurableStartupActivity : ProjectActivity {
ep.groupWeight = 40
}
"com.jetbrains.python.configuration.PyIntegratedToolsModulesConfigurable" -> {
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
}
}
}
}
@@ -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"
}
@@ -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=<html><body>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=<html><body>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. <a href='https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#command-line-options'>Learn more</a><br>\
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
@@ -551,6 +551,8 @@
description="Enables incremental reparse for statement lists"/>
<registryKey key="python.strict.type.narrow" defaultValue="false"
description="Allows narrowing types exhaustively to Never"/>
<registryKey key="python.lsp.tools.enabled" defaultValue="true" restartRequired="true"
description="Enables lsp tools"/>
<virtualFileCustomDataProvider implementation="com.jetbrains.python.formatter.PyCodeStyleSettingsProvider"/>
</extensions>
@@ -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)
}
}
@@ -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
@@ -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<BlackFormatterConfiguration.CliOptionFlag>(
@@ -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<BlackFormatterConfiguration.CliOptionFlag>)
: TextFieldWithCompletion(project, provider, "", true, true, true) {
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
@@ -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) {
@@ -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<Sdk>, initialSelection: Sdk?): ComboBox<Sdk> {
val comboBox = ComboBox<Sdk>()
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)
}
@@ -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"),
}
@@ -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<State : PyLspToolConfiguration<State>> : PersistentStateComponent<State> {
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,
}
@@ -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<Any> {
val sdks = buildList {
project.pythonSdk?.let { add(it) }
project.modules.mapNotNullTo(this) { it.pythonSdk }
}.distinct()
return ComboBox<Any>().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)
}