mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-90401 Split Pyright tool into separate Pyright and Basedpyright tools
Replace the combined PyrightPyTool (which mixed pyright/basedpyright via aliases + installInfo) with two PyTool extensions, each with its own configuration, configurable and icon. Drop the now-unused aliases/installInfo from PyTool and collapse executableNames to a single executableName; uvx runs `uvx --from <pkg> <pkg>-langserver`, so the isUvxSupported flag is gone. Add a PyLspTool<C> base in python-pytools holding the shared LSP-tool wiring (configuration service, legacy-state migration, FUS snapshot, icon). The pyright family shares one LSP server: its descriptor binds to the first enabled tool (basedpyright over pyright), and toggling either restarts the shared provider. Keep UI off the core abstraction: drop detailConfigurable from PyTool in favour of a UI-side PyToolDetailConfigurableProvider, and resolve the status-bar icon from the tool rather than the running server name. IJ-MR-209748 GitOrigin-RevId: 98ec7f9924af30a86be8cfe71c468800a58a94f9
This commit is contained in:
committed by
intellij-monorepo-bot
parent
0555c45ca4
commit
fa17ab9390
@@ -9,6 +9,7 @@ import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.PyToolsState
|
||||
import com.intellij.python.pytools.configuration.ExecutableDiscoveryMode
|
||||
import com.intellij.python.pytools.ui.PyToolDetailConfigurableProvider
|
||||
import com.intellij.python.black.PyBlackBundle.message
|
||||
import com.intellij.python.black.configuration.BlackFormatterConfigurable
|
||||
import com.intellij.python.black.configuration.BlackFormatterConfiguration
|
||||
@@ -17,7 +18,7 @@ import org.jetbrains.annotations.ApiStatus
|
||||
import kotlin.io.path.Path
|
||||
|
||||
@ApiStatus.Internal
|
||||
class BlackPyTool : PyTool {
|
||||
class BlackPyTool : PyTool, PyToolDetailConfigurableProvider {
|
||||
override val presentableName: String = "Black"
|
||||
override val description: String get() = message("black.tool.description")
|
||||
override val packageName: PyPackageName = PyPackageName.from("black")
|
||||
@@ -45,7 +46,7 @@ class BlackPyTool : PyTool {
|
||||
return entry
|
||||
}
|
||||
|
||||
override val detailConfigurable: (Project) -> UnnamedConfigurable = ::BlackFormatterConfigurable
|
||||
override fun createConfigurable(project: Project): UnnamedConfigurable = BlackFormatterConfigurable(project)
|
||||
|
||||
override fun summaryFor(project: Project): String {
|
||||
val cfg = BlackFormatterConfiguration.getBlackConfiguration(project)
|
||||
|
||||
@@ -91,6 +91,8 @@ jvm_library(
|
||||
"//python/python-pyproject:pyproject_test_lib",
|
||||
"//python/python-pytools:pytools",
|
||||
"//python/python-pytools:pytools_test_lib",
|
||||
"//python/python-pytools-ui:pytools-ui",
|
||||
"//python/python-pytools-ui:pytools-ui_test_lib",
|
||||
"//python/python-sdk:sdk",
|
||||
"//python/python-sdk:sdk_test_lib",
|
||||
"//python/python-test-env/junit5",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<orderEntry type="module" module-name="intellij.python.psi" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.pyproject" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.pytools" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.pytools.ui" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.sdk" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.test.env.junit5" scope="TEST" />
|
||||
</component>
|
||||
|
||||
@@ -18,12 +18,13 @@
|
||||
<module name="intellij.platform.testFramework.common"/>
|
||||
<module name="intellij.platform.testFramework.junit5"/>
|
||||
<module name="intellij.python.black"/>
|
||||
<module name="intellij.python.community.common"/>
|
||||
<module name="intellij.python.community"/>
|
||||
<module name="intellij.python.community.common"/>
|
||||
<module name="intellij.python.community.impl"/>
|
||||
<module name="intellij.python.psi"/>
|
||||
<module name="intellij.python.pyproject"/>
|
||||
<module name="intellij.python.pytools"/>
|
||||
<module name="intellij.python.pytools.ui"/>
|
||||
<module name="intellij.python.sdk"/>
|
||||
<module name="intellij.python.test.env.junit5"/>
|
||||
</dependencies>
|
||||
|
||||
+9
-9
@@ -2,11 +2,10 @@
|
||||
package com.intellij.python.pytools.ui
|
||||
|
||||
import com.intellij.openapi.options.BoundConfigurable
|
||||
import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.lsp.PyLspToolSettings
|
||||
import com.intellij.python.pytools.lsp.PyLspTool
|
||||
import com.intellij.python.pytools.lsp.PyLspToolConfiguration
|
||||
import com.intellij.python.pytools.statistics.PyToolActionSource
|
||||
import com.intellij.python.pytools.statistics.PyToolUsagesCollector
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
@@ -14,21 +13,22 @@ import com.intellij.ui.dsl.builder.panel
|
||||
|
||||
/**
|
||||
* Shared base for every LSP-backed tool's detail configurable. Subclasses supply only the
|
||||
* per-project [settings] service, an optional [inlayHintLabel] override (Pyright uses a
|
||||
* tool-specific string), and optional tool-specific rows via [extraRows] (Ruff uses this for
|
||||
* concrete [PyLspTool] (whose [PyLspTool.configuration] supplies [settings]), an optional
|
||||
* [inlayHintLabel] override, and optional tool-specific rows via [extraRows] (Ruff uses this for
|
||||
* its `formatting` and `sortImports` checkboxes).
|
||||
*
|
||||
* Logging happens automatically in [apply] via [PyToolUsagesCollector.Helper.logConfigurationChanged],
|
||||
* which pulls field values from [PyTool.configurationFusSnapshot]. A new tool that extends this
|
||||
* which pulls field values from the tool's `configurationFusSnapshot`. A new tool that extends this
|
||||
* base therefore cannot accidentally skip FUS reporting — there is no per-subclass `apply` to
|
||||
* forget.
|
||||
*/
|
||||
abstract class PyLspToolDetailConfigurable(
|
||||
abstract class PyLspToolDetailConfigurable<C : PyLspToolConfiguration<C>>(
|
||||
protected val project: Project,
|
||||
private val tool: PyTool,
|
||||
private val tool: PyLspTool<C>,
|
||||
) : BoundConfigurable(tool.presentableName) {
|
||||
|
||||
protected abstract val settings: PyLspToolSettings
|
||||
/** This tool's settings, taken straight from [PyLspTool.configuration] (the single source). */
|
||||
protected val settings: C get() = tool.configuration(project)
|
||||
|
||||
/** Label used for the inlay-hint row. Override when the tool needs a custom string. */
|
||||
protected open val inlayHintLabel: String = PyToolsUiBundle.message("checkbox.inlay.hints")
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.pytools.ui
|
||||
|
||||
import com.intellij.openapi.options.UnnamedConfigurable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.python.pytools.lsp.PyLspToolSettings
|
||||
|
||||
/**
|
||||
* Implemented by a `com.intellij.python.pytools.PyTool` that contributes a detail panel to the
|
||||
* External Tools table's Edit dialog. Kept out of `PyTool` itself so the core tool abstraction stays
|
||||
* free of UI types; the table discovers it with `tool as? PyToolDetailConfigurableProvider`.
|
||||
*/
|
||||
interface PyToolDetailConfigurableProvider {
|
||||
fun createConfigurable(project: Project): UnnamedConfigurable
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared comma-separated summary of the standard LSP feature toggles, for `PyTool.summaryFor`.
|
||||
* Tools with extra toggles (e.g. Ruff) build their own.
|
||||
*/
|
||||
fun pyLspToolFeaturesSummary(settings: PyLspToolSettings): @NlsSafe String = buildList {
|
||||
if (settings.inspections) add(PyToolsUiBundle.message("checkbox.inspections"))
|
||||
if (settings.completions == true) add(PyToolsUiBundle.message("checkbox.completions"))
|
||||
if (settings.inlayHints == true) add(PyToolsUiBundle.message("checkbox.inlay.hints"))
|
||||
if (settings.documentation == true) add(PyToolsUiBundle.message("checkbox.documentation"))
|
||||
}.joinToString(", ")
|
||||
+1
-1
@@ -6,6 +6,6 @@ import com.jetbrains.python.packaging.management.PythonPackageManager
|
||||
import com.jetbrains.python.packaging.management.getInstalledPackageSnapshot
|
||||
|
||||
fun PythonPackageManager.getInstalledToolPackage(pyTool: PyTool): PythonPackage? {
|
||||
return pyTool.aliases.firstNotNullOfOrNull { getInstalledPackageSnapshot(it.name) }
|
||||
return getInstalledPackageSnapshot(pyTool.packageName.name)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -176,7 +176,7 @@ internal class ToolCellRenderer(private val host: ToolCellHost) : JPanel(null),
|
||||
// handler exits early, and the cursor stays default. Same rule mirrored in
|
||||
// hover hit-testing and the table mouseClicked listener.
|
||||
paintGear = (row == host.hoveredRow) && (toolRow?.staged?.enabled == true)
|
||||
gearEnabled = toolRow?.tool?.detailConfigurable != null
|
||||
gearEnabled = toolRow?.detailConfigurableProvider != null
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -22,6 +22,7 @@ import com.intellij.python.pytools.findExecutableInPath
|
||||
import com.intellij.python.pytools.findExecutableInSdk
|
||||
import com.jetbrains.python.sdk.pyInterpreterPresentation
|
||||
import com.intellij.python.pytools.ui.PyToolsUiBundle
|
||||
import com.intellij.python.pytools.ui.PyToolDetailConfigurableProvider
|
||||
import com.intellij.python.pytools.ui.icons.PythonPytoolsUIIcons
|
||||
import com.jetbrains.python.Result
|
||||
import com.intellij.python.pytools.validateCustomPath
|
||||
@@ -91,7 +92,10 @@ internal class ToolRow(
|
||||
* glyph next to `Sdk`, plus a tooltip listing the resolved binary path per SDK.
|
||||
*/
|
||||
var sdkAvailability: SdkAvailability? = null,
|
||||
)
|
||||
) {
|
||||
/** This tool's detail-panel provider, or `null` when the tool has no detail configurable. */
|
||||
val detailConfigurableProvider: PyToolDetailConfigurableProvider? = tool as? PyToolDetailConfigurableProvider
|
||||
}
|
||||
|
||||
/**
|
||||
* Project-SDK detection snapshot for one [ToolRow]: an ordered list of SDKs with the tool's
|
||||
|
||||
+3
-3
@@ -241,7 +241,7 @@ internal class PyExternalToolsTable(
|
||||
// opens the detail dialog. Disabled tools surface no actions — including double
|
||||
// click — to match the missing gear icon and keep the row visually inert.
|
||||
val row = rows[viewRow]
|
||||
if (row.tool.detailConfigurable == null || !row.staged.enabled) return
|
||||
if (row.detailConfigurableProvider == null || !row.staged.enabled) return
|
||||
val onGear = isOverGearIcon(e, viewRow)
|
||||
if (onGear || e.clickCount == 2) {
|
||||
openDetailDialog(row)
|
||||
@@ -304,7 +304,7 @@ internal class PyExternalToolsTable(
|
||||
// visibly behaves like a clickable affordance. Tools without settings get a disabled
|
||||
// gear and keep the default cursor.
|
||||
val overGear = effective >= 0 &&
|
||||
rows[effective].tool.detailConfigurable != null &&
|
||||
rows[effective].detailConfigurableProvider != null &&
|
||||
isOverGearIcon(e, effective)
|
||||
val isIconWithAction = pathIconAtHover(pathEffective).let {
|
||||
it == PathIconKind.INSTALL || it == PathIconKind.UPGRADE || it == PathIconKind.RESET
|
||||
@@ -455,7 +455,7 @@ internal class PyExternalToolsTable(
|
||||
|
||||
/** Open the per-tool detail dialog for [toolRow]; refresh the row on commit. */
|
||||
private fun openDetailDialog(toolRow: ToolRow) {
|
||||
val configurable = toolRow.detail ?: toolRow.tool.detailConfigurable?.invoke(project) ?: return
|
||||
val configurable = toolRow.detail ?: toolRow.detailConfigurableProvider?.createConfigurable(project) ?: return
|
||||
toolRow.detail = configurable
|
||||
val component = configurable.createComponent() ?: return
|
||||
configurable.reset()
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ private fun lookupStrategyText(mode: com.intellij.python.pytools.configuration.E
|
||||
*/
|
||||
private fun toolColumnTooltip(toolRow: ToolRow, host: TooltipHost, eventX: Int, cellRect: Rectangle): String {
|
||||
val onGear = isOverIcon(eventX, cellRect, PythonPytoolsUIIcons.Settings.iconWidth)
|
||||
if (onGear && toolRow.staged.enabled && toolRow.tool.detailConfigurable != null) {
|
||||
if (onGear && toolRow.staged.enabled && toolRow.detailConfigurableProvider != null) {
|
||||
return PyToolsUiBundle.message("settings.external.tools.edit.tooltip", toolRow.tool.presentableName)
|
||||
}
|
||||
// Match the cell-rendering rule: a disabled tool's options aren't surfaced anywhere — its
|
||||
|
||||
+3
-18
@@ -109,25 +109,10 @@ internal class UvController(
|
||||
private var attemptedUpgrades: Set<String> = emptySet()
|
||||
|
||||
/**
|
||||
* Package name uv knows this row's tool by — a single value, never a set. Derived from the
|
||||
* basename of the detected binary, matched against the tool's `aliases`: for Pyright
|
||||
* (aliases `[pyright, basedpyright]`) this resolves to whichever of the two was actually
|
||||
* found on disk, and every uv operation (lookup, install/upgrade, attempted-upgrade tracking)
|
||||
* targets that same package. When nothing is detected the row is showing INSTALL and we fall
|
||||
* back to `installInfo.packageName.name` — the package the INSTALL click would put on disk.
|
||||
* Package name uv knows this row's tool by. Each tool maps 1:1 to a single PyPI package, so every uv
|
||||
* operation (lookup, install/upgrade, attempted-upgrade tracking) targets the tool's package name.
|
||||
*/
|
||||
private fun ToolRow.uvPackageName(): String {
|
||||
val detectedPath = when (val pfv = pathFieldValue) {
|
||||
is PathFieldValue.Custom -> pfv.path
|
||||
is PathFieldValue.AutoDetected -> pfv.path
|
||||
else -> null
|
||||
}
|
||||
if (detectedPath != null) {
|
||||
val baseName = detectedPath.fileName.toString().removeSuffix(".exe")
|
||||
tool.aliases.firstOrNull { it.name == baseName }?.let { return it.name }
|
||||
}
|
||||
return tool.installInfo.packageName.name
|
||||
}
|
||||
private fun ToolRow.uvPackageName(): String = tool.packageName.name
|
||||
|
||||
/** True iff the detected package is currently installed by uv (per the cached list). */
|
||||
fun isUvManaged(toolRow: ToolRow): Boolean =
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
package com.intellij.python.pytools
|
||||
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.options.UnnamedConfigurable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.python.pytools.statistics.PyToolFusSnapshot
|
||||
@@ -10,15 +9,9 @@ import com.jetbrains.python.packaging.PyPackageName
|
||||
import org.jetbrains.annotations.Nls
|
||||
import com.intellij.openapi.util.Version as PlatformVersion
|
||||
|
||||
data class InstallInfo(
|
||||
val packageName: PyPackageName,
|
||||
val installHelp: @Nls String? = null,
|
||||
)
|
||||
|
||||
interface PyTool {
|
||||
val presentableName: @NlsSafe String
|
||||
val packageName: PyPackageName
|
||||
val aliases: List<PyPackageName> get() = listOf(packageName)
|
||||
|
||||
/**
|
||||
* One-line user-facing description of the tool (e.g. "Linter and code formatter for Python").
|
||||
@@ -27,8 +20,6 @@ interface PyTool {
|
||||
*/
|
||||
val description: @Nls String
|
||||
|
||||
val installInfo: InstallInfo get() = InstallInfo(packageName)
|
||||
|
||||
/**
|
||||
* Provides a unique identifier (python package name) for the feature usage statistics (FUS) system.
|
||||
* The identifier is dynamically derived from the first package name in the list of known package names.
|
||||
@@ -54,13 +45,6 @@ interface PyTool {
|
||||
*/
|
||||
fun migrateLegacyState(project: Project): PyToolsState.ToolEntry? = null
|
||||
|
||||
/**
|
||||
* Factory that builds the per-tool detail UI shown in the Edit dialog of the External Tools
|
||||
* table. `null` (the default) means the tool has no extra options — the table uses this to dim
|
||||
* and disable the gear icon for the row.
|
||||
*/
|
||||
val detailConfigurable: ((Project) -> UnnamedConfigurable)? get() = null
|
||||
|
||||
/**
|
||||
* Compact, comma-separated summary of currently-activated features for the External Tools table
|
||||
* (e.g. "Inspections, Formatting"). Returning an empty string hides the cell content for the
|
||||
|
||||
@@ -39,18 +39,17 @@ fun PyTool.isEnabledOn(project: Project): Boolean = getState(project).enabled
|
||||
|
||||
suspend fun PyTool.getExecutableWithBaseArgs(
|
||||
moduleOrProject: ModuleOrProject,
|
||||
executableNames: List<String> = aliases.map { it.name },
|
||||
executableName: String = packageName.name,
|
||||
workingDir: Path? = null,
|
||||
isUvxSupported: Boolean = true
|
||||
): PyResult<Pair<BinaryToExec, List<String>>> {
|
||||
val state = getState(moduleOrProject.project)
|
||||
|
||||
val toolBinaryPath = when (state.discoveryMode) {
|
||||
ExecutableDiscoveryMode.INTERPRETER -> {
|
||||
val pyRichSdk = moduleOrProject.moduleIfExists?.pythonSdk?.pyRichSdk()
|
||||
pyRichSdk?.let { findExecutableInSdk(it, executableNames) } ?: findExecutableInPath(state, executableNames)
|
||||
pyRichSdk?.let { findExecutableInSdk(it, executableName) } ?: findExecutableInPath(state, executableName)
|
||||
}
|
||||
ExecutableDiscoveryMode.PATH -> findExecutableInPath(state, executableNames)
|
||||
ExecutableDiscoveryMode.PATH -> findExecutableInPath(state, executableName)
|
||||
ExecutableDiscoveryMode.UVX -> null
|
||||
}
|
||||
|
||||
@@ -62,14 +61,13 @@ suspend fun PyTool.getExecutableWithBaseArgs(
|
||||
BinOnEel(toolBinaryPath, workDir = workDir).let { PyResult.success(it to emptyList()) }
|
||||
}
|
||||
else {
|
||||
if (!isUvxSupported) {
|
||||
return PyResult.localizedError(message("uvx.is.not.installed"))
|
||||
}
|
||||
|
||||
val uvxPath = localEel.exec.where("uvx")
|
||||
?: return PyResult.localizedError(message("uvx.is.not.installed"))
|
||||
|
||||
val uvxArgs = listOf(packageName.name)
|
||||
// `uvx <pkg>` only works when the package's entry point matches its name. When the executable
|
||||
// differs (e.g. pyright → pyright-langserver) uvx needs `--from <pkg> <executable>`.
|
||||
val uvxArgs = if (executableName == packageName.name) listOf(executableName)
|
||||
else listOf("--from", packageName.name, executableName)
|
||||
BinOnEel(uvxPath.asNioPath(), workDir = workDir).let { PyResult.success(it to uvxArgs) }
|
||||
}
|
||||
}
|
||||
@@ -116,11 +114,6 @@ suspend fun PyTool.resolveVersion(moduleOrProject: ModuleOrProject): PyResult<Ve
|
||||
return versionOutput.parseVersion(packageName.name)
|
||||
}
|
||||
|
||||
private fun EelOsFamily.getOsSpecificBinaryFileNames(executableNames: List<String>): Sequence<String> {
|
||||
return executableNames.asSequence().map { getOsSpecificBinaryName(it) }
|
||||
}
|
||||
|
||||
|
||||
private fun EelOsFamily.getOsSpecificBinaryName(binaryName: String): String = when (this) {
|
||||
EelOsFamily.Posix -> binaryName
|
||||
EelOsFamily.Windows -> "$binaryName.exe"
|
||||
@@ -129,22 +122,18 @@ private fun EelOsFamily.getOsSpecificBinaryName(binaryName: String): String = wh
|
||||
/**
|
||||
* only local sdks are supported currently
|
||||
*/
|
||||
fun PyTool.findExecutableInSdk(pyRichSdk: PyRichSdk, executableNames: List<String> = aliases.map { it.name }): Path? {
|
||||
fun PyTool.findExecutableInSdk(pyRichSdk: PyRichSdk, executableName: String = packageName.name): Path? {
|
||||
return pyRichSdk.pythonBinaryPath?.let { basePythonBinaryPath ->
|
||||
val osFamily = basePythonBinaryPath.getEelDescriptor().osFamily
|
||||
osFamily.getOsSpecificBinaryFileNames(executableNames).firstNotNullOfOrNull { binaryFileName ->
|
||||
basePythonBinaryPath.resolveSibling(binaryFileName).takeIf { it.isExecutable() }
|
||||
}
|
||||
basePythonBinaryPath.resolveSibling(osFamily.getOsSpecificBinaryName(executableName)).takeIf { it.isExecutable() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun PyTool.findExecutableInPath(state: PyToolsState.ToolEntry, executableNames: List<String> = aliases.map { it.name }): Path? {
|
||||
return state.customToolBinaryPath ?: findExecutableInPath(executableNames)
|
||||
private fun PyTool.findExecutableInPath(state: PyToolsState.ToolEntry, executableName: String = packageName.name): Path? {
|
||||
return state.customToolBinaryPath ?: findExecutableInPath(executableName)
|
||||
}
|
||||
|
||||
fun PyTool.findExecutableInPath(
|
||||
executableNames: List<String> = aliases.map { it.name },
|
||||
executableName: String = packageName.name,
|
||||
osFamily: EelOsFamily = LocalEelDescriptor.osFamily,
|
||||
): Path? = osFamily.getOsSpecificBinaryFileNames(executableNames).firstNotNullOfOrNull {
|
||||
PathEnvironmentVariableUtil.findInPath(it)?.toPath()
|
||||
}
|
||||
): Path? = PathEnvironmentVariableUtil.findInPath(osFamily.getOsSpecificBinaryName(executableName))?.toPath()
|
||||
|
||||
@@ -68,16 +68,8 @@ suspend fun BinaryToExec.getToolVersion(toolVersionPrefix: String): PyResult<Ver
|
||||
/**
|
||||
* Convenience: validate a user-supplied custom path for [this] tool against its known names.
|
||||
* Returns a localized failure if `<path> --version` does not match.
|
||||
*
|
||||
* The expected version-prefix is derived from [path]'s basename matched against [PyTool.aliases] —
|
||||
* required for tools where the primary [PyTool.packageName] differs from the actually-installed
|
||||
* alias (e.g. Pyright is integrated as `pyright` but `basedpyright --version` prints
|
||||
* `basedpyright X.Y.Z`, which would never match the `pyright` prefix). When the basename
|
||||
* matches no known alias we fall back to the primary [PyTool.packageName] for backward compatibility.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
suspend fun PyTool.validateCustomPath(path: Path): PyResult<Version> {
|
||||
val baseName = path.fileName.toString().removeSuffix(".exe")
|
||||
val versionPrefix = aliases.firstOrNull { it.name == baseName }?.name ?: packageName.name
|
||||
return BinOnEel(path).getToolVersion(versionPrefix)
|
||||
return BinOnEel(path).getToolVersion(packageName.name)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.pytools.lsp
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.PyToolsState
|
||||
import com.intellij.python.pytools.statistics.PyToolFusSnapshot
|
||||
import javax.swing.Icon
|
||||
|
||||
/**
|
||||
* Base for every LSP-backed [PyTool]. Captures the shared, non-UI wiring around a per-project
|
||||
* [PyLspToolConfiguration] — its [configuration] service is the single source of the tool's settings,
|
||||
* used here for legacy-state migration and the FUS snapshot, and by the detail UI in the UI module.
|
||||
*
|
||||
* UI concerns (the detail configurable, the features summary) deliberately live in the UI layer and
|
||||
* are not part of this base.
|
||||
*/
|
||||
abstract class PyLspTool<C : PyLspToolConfiguration<*>> : PyTool {
|
||||
/** Per-project settings service backing this tool — the single source of its configuration. */
|
||||
abstract fun configuration(project: Project): C
|
||||
|
||||
/** Icon shown for this tool's LSP server (status-bar widget, advertiser notification, ...). */
|
||||
abstract val icon: Icon
|
||||
|
||||
override fun migrateLegacyState(project: Project): PyToolsState.ToolEntry = configuration(project).migrateToPyToolState()
|
||||
|
||||
override fun configurationFusSnapshot(project: Project): PyToolFusSnapshot {
|
||||
val cfg = configuration(project)
|
||||
return super.configurationFusSnapshot(project).copy(
|
||||
inspections = cfg.inspections,
|
||||
completions = cfg.completions,
|
||||
inlayHints = cfg.inlayHints,
|
||||
documentation = cfg.documentation,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user