PY-80189 Replace PyPackagesTable with tree-based PyPackagesTreeTable.

Migrated to a tree-based UI for managing Python packages, replacing the previous table-based implementation. This change introduces `PyPackagesTreeTable`, consolidating UI functionality and improving package management UX. Removed redundant table-based classes and refactored associated functionality.


Merge-request: IJ-MR-164644
Merged-by: Timur Malanin <timur.malanin@jetbrains.com>

GitOrigin-RevId: 6e99398f0aa970806428221c194d09185554d2f5
This commit is contained in:
Timur Malanin
2025-06-16 13:38:02 +00:00
committed by intellij-monorepo-bot
parent f5dbbb76e7
commit 4cc34127cf
34 changed files with 1370 additions and 732 deletions
@@ -171,6 +171,11 @@ public class JBTreeTable extends JComponent implements TreePathBackgroundSupplie
return myTable;
}
@ApiStatus.Internal
public final @NotNull OnePixelSplitter getSplitter() {
return split;
}
public void setDefaultRenderer(@NotNull Class<?> columnClass, @NotNull TableCellRenderer renderer) {
myTable.setDefaultRenderer(columnClass,renderer);
}
@@ -778,7 +778,11 @@
dynamic="true"/>
<extensionPoint qualifiedName="Pythonid.PythonPackageRequiresExtractorProvider"
interface="com.jetbrains.python.packaging.packageRequires.PythonPackageRequiresExtractorProvider"
interface="com.jetbrains.python.packaging.packageRequirements.PythonPackageRequiresExtractorProvider"
dynamic="true"/>
<extensionPoint qualifiedName="Pythonid.PythonPackageRequirementsTreeExtractorProvider"
interface="com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractorProvider"
dynamic="true"/>
</extensionPoints>
@@ -886,6 +890,11 @@
<PythonPackageRequiresExtractorProvider implementation="com.jetbrains.python.poetry.packaging.PoetryRequiresExtractorProvider"/>
<PythonPackageRequiresExtractorProvider implementation="com.jetbrains.python.uv.packaging.UvPackageRequiresExtractorProvider"/>
<PythonPackageRequirementsTreeExtractorProvider implementation="com.jetbrains.python.poetry.packaging.PoetryPackageRequirementsTreeExtractorProvider"/>
<PythonPackageRequirementsTreeExtractorProvider implementation="com.jetbrains.python.uv.packaging.UvPackageRequirementsTreeExtractorProvider"/>
<systemPythonProvider implementation="com.jetbrains.python.sdk.uv.UvSystemPythonProvider"/>
<inspectionExtension implementation="com.jetbrains.python.sdk.configuration.PyInterpreterInspectionSuppressor"/>
@@ -1421,6 +1421,7 @@ python.toolwindow.packages.add.package.dialog.title=Install Package
python.toolwindow.packages.load.more=\u2026 {0} more items. Double click to show more.
python.toolwindow.packages.converting.description.progress=Reading package description
python.toolwindow.packages.documentation.local.interpreter=PyCharm need at least one local interpreter to display package documentation.
python.toolwindow.packages.sdk.label.html=<html>{0} <b>({1})</b></html>
python.packages.dictionary.name=Python Packages Dictionary
vcs.activate.virtualenv.checkbox.text=Activate virtualenv for hooks
@@ -7,8 +7,10 @@ import com.jetbrains.python.packaging.pyRequirementVersionSpec
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.requirement.PyRequirementRelation
import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec
import com.jetbrains.python.psi.icons.PythonPsiApiIcons
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import javax.swing.Icon
@ApiStatus.Internal
@JvmInline
@@ -26,6 +28,8 @@ open class PythonPackage(name: String, val version: String, val isEditableMode:
val name: String = NormalizedPythonPackageName.from(name).name
val presentableName: String = name
@ApiStatus.Internal
open val sourceRepoIcon: Icon = PythonPsiApiIcons.Python
override fun toString(): String {
return "PythonPackage(name='$name', version='$version')"
@@ -2,15 +2,24 @@
package com.jetbrains.python.packaging.conda
import com.jetbrains.python.PyBundle
import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.psi.icons.PythonPsiApiIcons
import javax.swing.Icon
class CondaPackage(
name: String, version: String,
editableMode: Boolean,
val installedWithPip: Boolean = false,
) : PythonPackage(name, version, editableMode) {
override val sourceRepoIcon: Icon = if (installedWithPip) {
PythonPsiApiIcons.Python
} else {
PythonIcons.Python.Anaconda
}
override fun toString(): String {
return "CondaPackage(name='$name', version='$version', installedWithPip=$installedWithPip)"
}
@@ -1,5 +1,5 @@
// 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.packaging.packageRequires
package com.jetbrains.python.packaging.packageRequirements
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.module.Module
@@ -9,20 +9,20 @@ import com.jetbrains.python.packaging.common.PythonPackage
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface PythonPackageRequiresExtractor {
interface PythonPackageRequirementExtractor {
suspend fun extract(pkg: PythonPackage, module: Module): List<NormalizedPythonPackageName>
companion object {
fun forSdk(sdk: Sdk): PythonPackageRequiresExtractor? =
PythonPackageRequiresExtractorProvider.EP_NAME.extensionList.firstNotNullOf { it.createExtractor(sdk) }
fun forSdk(sdk: Sdk): PythonPackageRequirementExtractor? =
PythonPackageRequiresExtractorProvider.EP_NAME.extensionList.firstNotNullOfOrNull { it.createExtractor(sdk) }
}
}
@ApiStatus.Internal
interface PythonPackageRequiresExtractorProvider {
fun createExtractor(sdk: Sdk): PythonPackageRequiresExtractor?
fun createExtractor(sdk: Sdk): PythonPackageRequirementExtractor?
companion object {
val EP_NAME: ExtensionPointName<PythonPackageRequiresExtractorProvider> = ExtensionPointName.create<PythonPackageRequiresExtractorProvider>("Pythonid.PythonPackageRequiresExtractorProvider")
@@ -0,0 +1,84 @@
// 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.packaging.packageRequirements
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
import com.jetbrains.python.packaging.common.PythonPackage
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface PythonPackageRequirementsTreeExtractor {
suspend fun extract(pkg: PythonPackage, module: Module): PackageNode
companion object {
private val treeParser = TreeParser()
fun forSdk(sdk: Sdk): PythonPackageRequirementsTreeExtractor? =
PythonPackageRequirementsTreeExtractorProvider.EP_NAME.extensionList
.firstNotNullOfOrNull { it.createExtractor(sdk) }
fun parseTree(lines: List<String>): PackageNode = treeParser.parseTree(lines)
}
}
@ApiStatus.Internal
interface PythonPackageRequirementsTreeExtractorProvider {
fun createExtractor(sdk: Sdk): PythonPackageRequirementsTreeExtractor?
companion object {
private const val EP_NAME_VALUE = "Pythonid.PythonPackageRequirementsTreeExtractorProvider"
val EP_NAME: ExtensionPointName<PythonPackageRequirementsTreeExtractorProvider> =
ExtensionPointName.create(EP_NAME_VALUE)
}
}
@ApiStatus.Internal
data class PackageNode(
val name: NormalizedPythonPackageName,
val children: MutableList<PackageNode> = mutableListOf(),
)
@ApiStatus.Internal
class TreeParser {
private data class ParseResult(
val node: PackageNode,
val nextIndex: Int,
)
fun parseTree(lines: List<String>): PackageNode {
val (node, _) = parseLevel(lines, calculateIndentLevel(lines.first()), 0)
return node
}
private fun parseLevel(lines: List<String>, startIndent: Int, index: Int): ParseResult {
val line = lines[index]
val name = extractPackageName(line)
val node = PackageNode(NormalizedPythonPackageName.from(name))
var currentIndex = index + 1
while (currentIndex < lines.size && calculateIndentLevel(lines[currentIndex]) > startIndent) {
val result = parseLevel(lines, calculateIndentLevel(lines[currentIndex]), currentIndex)
node.children.add(result.node)
currentIndex = result.nextIndex
}
return ParseResult(node, currentIndex)
}
private fun calculateIndentLevel(line: String): Int {
val indentMatch = TREE_LINE_REGEX.find(line)?.value ?: ""
return indentMatch.chunked(4).count { it.isNotBlank() }
}
private fun extractPackageName(line: String): String {
val clean = line.replaceFirst(TREE_LINE_REGEX, "").trimStart()
return clean.split(SPACE_DELIMITER, limit = 2)[0]
.substringBefore(VERSION_DELIMITER)
}
companion object Constants {
private val TREE_LINE_REGEX = Regex("""^[\s│|`]*[├└`|][-─]+ """)
private const val SPACE_DELIMITER = ' '
private const val VERSION_DELIMITER = '['
}
}
@@ -1,202 +0,0 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.packaging.toolwindow
import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.ui.JBColor
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.repository.InstalledPyPackagedRepository
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.toolwindow.model.*
import com.jetbrains.python.packaging.toolwindow.packages.PyPackagingTableGroup
import com.jetbrains.python.packaging.toolwindow.packages.table.PyPackagesTable
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import java.awt.Rectangle
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JTable
class PyPackagingTablesView(
private val project: Project,
private val container: JPanel,
private val controller: PyPackagingToolWindowPanel,
) {
private val repositories: MutableList<PyPackagingTableGroup> = mutableListOf()
private val installedPackages = PyPackagingTableGroup(
InstalledPyPackagedRepository(),
PyPackagesTable(project, this, controller))
private val invalidRepositories: MutableMap<String, JPanel> = mutableMapOf()
init {
installedPackages.addTo(container)
installedPackages.expand()
}
fun showSearchResult(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) {
updatePackages(installed, repoData)
installedPackages.expand()
installedPackages.updateHeaderText(installed.size)
val tableToData = repositories.map { repo -> repo to repoData.find { it.repository.name == repo.name }!! }
tableToData.forEach { (table, data) ->
table.updateHeaderText(data.packages.size + data.moreItems)
table.expand()
}
// todo[akniazev]: selecting a package in 'installed' list might make more sense
tableToData
.firstOrNull { (_, data) -> data.exactMatch != -1 }
?.let { selectPackage(it.second) }
}
fun resetSearch(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>, currentSdk: Sdk?) {
updatePackages(installed, repoData)
installedPackages.expand()
installedPackages.setSdkToHeader(currentSdk?.name)
repositories.forEach {
it.collapse()
it.updateHeaderText(null)
}
container.scrollRectToVisible(Rectangle(0, 0))
}
private fun updatePackages(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) {
installedPackages.table.items = installed
val (validRepoData, invalid) = repoData.partition { it !is PyInvalidRepositoryViewData }
for (data in validRepoData) {
val existingRepo = findTableForRepo(data.repository)
val withExpander = if (data.moreItems > 0) data.packages + listOf(ExpandResultNode(data.moreItems, data.repository)) else data.packages
if (existingRepo != null) {
// recreate order of the repositories -- it might have changed in the package manager (e.g. Sdk switch)
existingRepo.removeFrom(container)
val selectedItem = existingRepo.table.selectedItem()
existingRepo.items = withExpander
existingRepo.addTo(container)
selectedItem?.let { existingRepo.table.selectPackage(it) }
}
else {
val newTable = PyPackagesTable(project, this, controller)
newTable.items = withExpander
val newTableGroup = PyPackagingTableGroup(data.repository, newTable)
repositories.add(newTableGroup)
newTableGroup.addTo(container)
}
}
val existingRepositories = validRepoData.map { it.repository.name }
val removedRepositories = repositories.filter { it.name !in existingRepositories }
removedRepositories.forEach { it.removeFrom(container) }
repositories.removeAll(removedRepositories)
@Suppress("UNCHECKED_CAST")
refreshInvalidRepositories(invalid as List<PyInvalidRepositoryViewData>)
}
private fun refreshInvalidRepositories(invalid: List<PyInvalidRepositoryViewData>) {
val invalidRepoNames = invalid.map { it.repository.name }
invalidRepositories.forEach { container.remove(it.value) }
invalidRepositories.keys.removeIf { it !in invalidRepoNames }
invalid.asSequence()
.map { it.repository.name!! }
.filterNot { it in invalidRepositories }
.map {
val label = JLabel(message("python.toolwindow.packages.custom.repo.invalid", it)).apply {
foreground = JBColor.RED
icon = AllIcons.General.Error
}
it to PyPackagesUiComponents.headerPanel(label, null)
}
.forEach {
invalidRepositories[it.first] = it.second
}
invalidRepositories.forEach { container.add(it.value) }
}
fun selectPackage(packageName: String) {
val repos = getRepos()
for (repo in repos) {
val pyPackage = repo.items.firstOrNull { it.name == packageName } ?: continue
repo.table.selectPackage(pyPackage)
}
}
private fun selectPackage(matchData: PyPackagesViewData) {
val tableWithMatch = findTableForRepo(matchData.repository)!!.table
val exactMatch = matchData.exactMatch
tableWithMatch.setRowSelectionInterval(exactMatch, exactMatch)
}
fun removeSelectionNotFormTable(table: JTable) {
if (table != installedPackages.table) installedPackages.table.clearSelection()
repositories.asSequence()
.filter { it.table != table }
.forEach { it.table.clearSelection() }
}
private fun findTableForRepo(repository: PyPackageRepository) = repositories.find { it.name == repository.name }
fun selectNextFrom(currentTable: JTable) {
val targetGroup = when (currentTable) {
installedPackages.table -> repositories.firstOrNull()
else -> {
val currentIndex = repositories.indexOfFirst { it.table == currentTable }
if (currentIndex + 1 != repositories.size) repositories[currentIndex + 1]
else return
}
}
targetGroup?.apply {
if (items.isNotEmpty()) {
expand()
table.setRowSelectionInterval(0, 0)
table.requestFocus()
}
}
}
fun selectPreviousOf(currentTable: JTable) {
val targetGroup = when (currentTable) {
installedPackages.table -> return
repositories.firstOrNull()?.table -> installedPackages
else -> {
val currentIndex = repositories.indexOfFirst { it.table == currentTable }
repositories[currentIndex - 1]
}
}
targetGroup.apply {
if (items.isNotEmpty()) {
expand()
val newIndex = items.size - 1
table.setRowSelectionInterval(newIndex, newIndex)
table.requestFocus()
}
}
}
fun getSelectedPackages(): List<DisplayablePackage> {
val repos = getRepos()
return repos.flatMap { it.table.selectedItems() }
}
fun collapseAll() {
getRepos().forEach { it.collapse() }
}
private fun getRepos() = listOf(installedPackages) + repositories
}
@@ -10,9 +10,11 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.options.ex.SingleConfigurableEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootEvent
import com.intellij.openapi.roots.ModuleRootListener
@@ -21,45 +23,41 @@ import com.intellij.openapi.vfs.VirtualFileManager
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.getOrThrow
import com.jetbrains.python.packaging.*
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageManagementListener
import com.jetbrains.python.packaging.conda.CondaPackage
import com.jetbrains.python.packaging.management.*
import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI
import com.jetbrains.python.packaging.management.ui.updatePackagesByNamesBackground
import com.jetbrains.python.packaging.repository.PyPackageRepositories
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.repository.PyRepositoriesList
import com.jetbrains.python.packaging.repository.checkValid
import com.jetbrains.python.packaging.packageRequirements.PackageNode
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractor
import com.jetbrains.python.packaging.repository.*
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector
import com.jetbrains.python.packaging.toolwindow.model.*
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.statistics.modules
import kotlinx.coroutines.*
import org.jetbrains.annotations.Nls
@Service(Service.Level.PROJECT)
class PyPackagingToolWindowService(val project: Project, val serviceScope: CoroutineScope) : Disposable {
private var toolWindowPanel: PyPackagingToolWindowPanel? = null
private var installedPackages: Map<String, InstalledPackage> = emptyMap()
private var searchJob: Job? = null
private var currentQuery: String = ""
internal var currentSdk: Sdk? = null
private var managerUI: PythonPackageManagerUI? = null
private lateinit var managerUI: PythonPackageManagerUI
private val manager: PythonPackageManager?
get() = managerUI?.manager
get() = managerUI.manager
private val invalidRepositories: List<PyInvalidRepositoryViewData>
get() = service<PyPackageRepositories>().invalidRepositories.map(::PyInvalidRepositoryViewData)
fun initialize(toolWindowPanel: PyPackagingToolWindowPanel) {
this.toolWindowPanel = toolWindowPanel
serviceScope.launch(Dispatchers.IO) {
@@ -73,22 +71,22 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
val packageManager = manager ?: return null
return withContext(Dispatchers.IO) {
PythonPackagesToolwindowStatisticsCollector.requestDetailsEvent.log(project)
val pkgName = NormalizedPythonPackageName.from(selectedPackage.name).name
val spec = when (selectedPackage) {
is InstalledPackage -> packageManager.findPackageSpecification(selectedPackage.name)
is InstallablePackage -> selectedPackage.repository.findPackageSpecification(selectedPackage.name)
is ExpandResultNode -> selectedPackage.repository.findPackageSpecification(selectedPackage.name)
else -> error("Invalidate package spec ${selectedPackage::class.java.name}")
is InstalledPackage -> packageManager.findPackageSpecification(pkgName)
is InstallablePackage -> selectedPackage.repository.findPackageSpecification(pkgName)
is ExpandResultNode -> selectedPackage.repository.findPackageSpecification(pkgName)
is RequirementPackage -> selectedPackage.repository.findPackageSpecification(pkgName)
}
if (spec == null) {
return@withContext null
}
spec.let { packageManager.repositoryManager.getPackageDetails(it).getOrThrow() } ?: error("Invalid package specification")
spec.let { packageManager.repositoryManager.getPackageDetails(it).getOrThrow() }
}
}
fun handleSearch(query: String) {
val manager = manager ?: return
val prevSelected = toolWindowPanel?.getSelectedPackage()
@@ -130,20 +128,27 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
suspend fun installPackage(installRequest: PythonPackageInstallRequest, options: List<String> = emptyList()) {
PythonPackagesToolwindowStatisticsCollector.installPackageEvent.log(project)
managerUI?.installPackagesBackground(installRequest, options)?.let {
managerUI.installPackagesBackground(installRequest, options)?.let {
handleActionCompleted(message("python.packaging.notification.installed", installRequest.title))
}
}
suspend fun installPackage(pkg: PythonPackage, options: List<String> = emptyList()) {
val installRequest = manager?.findPackageSpecification(pkg.name, pkg.version)?.toInstallRequest() ?: return
PythonPackagesToolwindowStatisticsCollector.installPackageEvent.log(project)
managerUI.installPackagesBackground(installRequest, options)?.let {
handleActionCompleted(message("python.packaging.notification.installed", installRequest.title))
}
}
suspend fun deletePackage(vararg selectedPackages: InstalledPackage) {
PythonPackagesToolwindowStatisticsCollector.uninstallPackageEvent.log(project)
managerUI?.uninstallPackagesBackground(selectedPackages.map { it.instance.name }) ?: return
managerUI.uninstallPackagesBackground(selectedPackages.map { it.instance.name }) ?: return
handleActionCompleted(message("python.packaging.notification.deleted", selectedPackages.joinToString(", ") { it.name }))
}
suspend fun updatePackages(vararg packages: String) {
managerUI?.updatePackagesByNamesBackground(packages.toList()) ?: return
managerUI.updatePackagesByNamesBackground(packages.toList()) ?: return
val singlePackage = packages.singleOrNull()
@@ -154,6 +159,8 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
else {
handleActionCompleted(message("python.packaging.notification.all.updated"))
}
refreshInstalledPackages()
}
@@ -172,10 +179,10 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
val previousSdk = currentSdk
currentSdk = sdk
managerUI = sdk?.let { PythonPackageManagerUI.forSdk(project, it) }
if (sdk == null) {
return
}
managerUI = PythonPackageManagerUI.forSdk(project, sdk)
withContext(Dispatchers.EDT) {
@@ -226,22 +233,127 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
})
}
suspend fun refreshInstalledPackages() {
val packages = manager?.listInstalledPackages()?.map {
val spec = manager?.findPackageSpecification(it.name, it.version)
val repository = spec?.repository
val nextVersionRaw = manager?.listOutdatedPackagesSnapshot()[it.name]?.latestVersion
val nextVersion = nextVersionRaw?.let { PyPackageVersionNormalizer.normalize(it) }
InstalledPackage(it, repository, nextVersion)
} ?: emptyList()
/**
* Information about a Python package and its state in the package management system.
*
* @property packageData Information about the currently installed package
* @property repository The package repository where this package is hosted
* @property nextVersion The next available version of the package in its repository.
* Null if the package is already at its latest version
* @property dependencies List of packages that this package depends on
*/
data class PackageInfo(
val packageData: InstalledPackage,
val repository: PyPackageRepository,
val nextVersion: String?,
val dependencies: List<RequirementPackage>
)
installedPackages = packages.associateBy { it.name }
suspend fun refreshInstalledPackages() {
val sdk = currentSdk ?: return
val targetModule = findTargetModule(sdk) ?: return
val manager = manager ?: return
withContext(Dispatchers.Default) {
val declaredPackages = manager.reloadDependencies()
val installedDeclaredPackages = findInstalledDeclaredPackages(declaredPackages)
val treeExtractor = PythonPackageRequirementsTreeExtractor.forSdk(sdk)
val packagesWithDependencies = if (treeExtractor != null) {
processPackagesWithRequirementsTree(
installedDeclaredPackages,
treeExtractor,
targetModule
)
} else {
emptyList()
}
val standalonePackages = findStandalonePackages(packagesWithDependencies)
installedPackages = (packagesWithDependencies + standalonePackages)
.associateBy { it.name }
}
withContext(Dispatchers.Main) {
handleSearch(query = currentQuery)
}
}
private fun findTargetModule(sdk: Sdk): Module? =
project.modules.find { it.pythonSdk == sdk }
private suspend fun findInstalledDeclaredPackages(declaredPackages: List<PythonPackage>): List<PythonPackage> =
manager?.listInstalledPackages()?.filter {
it.name in declaredPackages.map { pkg -> pkg.name }
} ?: emptyList()
private suspend fun processPackagesWithRequirementsTree(
packages: List<PythonPackage>,
treeExtractor: PythonPackageRequirementsTreeExtractor,
targetModule: Module,
): List<InstalledPackage> {
return packages.mapNotNull { pkg ->
val tree = treeExtractor.extract(pkg, targetModule)
createInstalledPackageFromTree(pkg, tree)
}
}
private suspend fun createInstalledPackageFromTree(
pkg: PythonPackage,
tree: PackageNode,
): InstalledPackage? {
val manager = manager ?: return null
val spec = manager.findPackageSpecification(pkg.name, pkg.version)
val repository = spec?.repository
val nextVersionRaw = manager.listOutdatedPackagesSnapshot()[pkg.name]?.latestVersion
val nextVersion = nextVersionRaw?.let { PyPackageVersionNormalizer.normalize(it) }
val requirements = createRequirementsFromTree(tree.children, repository ?: PyPIPackageRepository)
return InstalledPackage(pkg, repository, nextVersion, requirements)
}
private suspend fun createRequirementsFromTree(
nodes: List<PackageNode>,
repository: PyPackageRepository,
): List<RequirementPackage> {
val manager = manager ?: return emptyList()
return nodes.mapNotNull { node ->
val packageName = node.name.name
val dependencyPkg = manager.listInstalledPackages().find { it.name == packageName }
dependencyPkg?.let { depPkg ->
val childRequirements = createRequirementsFromTree(node.children, repository)
RequirementPackage(depPkg, repository, childRequirements)
}
}
}
private fun collectPackageNamesRecursively(
pkg: DisplayablePackage,
): List<String> {
return listOf(pkg.name) + pkg.getRequirements().flatMap { requirement ->
collectPackageNamesRecursively(requirement)
}
}
private suspend fun findStandalonePackages(
processedPackages: List<InstalledPackage>,
): List<InstalledPackage> {
val manager = manager ?: return emptyList()
val processedPackageNames = processedPackages.flatMap { pkg ->
collectPackageNamesRecursively(pkg)
}.toSet()
return manager.listInstalledPackages()
.filter { it.name !in processedPackageNames }
.map { pkg ->
val repository = installedPackages.values.find { it.name == pkg.name }?.repository ?: PyPIPackageRepository
val nextVersionRaw = manager.listOutdatedPackagesSnapshot()[pkg.name]?.latestVersion
val nextVersion = nextVersionRaw?.let { PyPackageVersionNormalizer.normalize(it) }
InstalledPackage(pkg, repository, nextVersion, emptyList())
}
}
private suspend fun handleActionCompleted(text: @Nls String) {
VirtualFileManager.getInstance().asyncRefresh()
showPackagingNotification(text)
@@ -274,7 +386,6 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
return PyPackagesViewData(repository, shownPackages, exactMatch, moreItems)
}
override fun dispose() {
searchJob?.cancel()
serviceScope.cancel()
@@ -282,7 +393,7 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
fun reloadPackages() {
serviceScope.launch(Dispatchers.Default) {
managerUI?.reloadPackagesBackground()
managerUI.reloadPackagesBackground()
refreshInstalledPackages()
}
}
@@ -367,4 +478,4 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
return nameComparator
}
}
}
}
@@ -0,0 +1,228 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.packaging.toolwindow
import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.ui.JBColor
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.repository.InstalledPyPackagedRepository
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.toolwindow.model.*
import com.jetbrains.python.packaging.toolwindow.packages.PyPackagingTreeGroup
import com.jetbrains.python.packaging.toolwindow.packages.tree.PyPackagesTreeTable
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import java.awt.Rectangle
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JLabel
import javax.swing.JPanel
internal class PyPackagingTreeView(
private val project: Project,
private val container: JPanel,
private val controller: PyPackagingToolWindowPanel,
) {
private val repositories: MutableList<PyPackagingTreeGroup> = mutableListOf()
private val installedPackages =
PyPackagingTreeGroup(
InstalledPyPackagedRepository(),
PyPackagesTreeTable(project, controller),
container
)
private val invalidRepositories: MutableMap<String, JPanel> = mutableMapOf()
private var isSyncingTreeSelection = false
init {
installedPackages.addTo(container)
installedPackages.expand()
container.addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
synchronizeScrollPaneSize()
}
})
getRepos().forEach { tree ->
tree.tree.tree.addTreeSelectionListener {
syncTreeSelection(tree.tree)
}
}
}
fun showSearchResult(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) {
updatePackages(installed, repoData)
installedPackages.expand()
installedPackages.updatePreferredSize()
installedPackages.updateHeaderText(installed.size)
val tableToData = repositories.map { repo -> repo to repoData.find { it.repository.name == repo.repositoryName }!! }
tableToData.forEach { (table, data) ->
table.updateHeaderText(data.packages.size + data.moreItems)
table.expand()
}
synchronizeScrollPaneSize()
val exactMatchPackageName = tableToData
.firstOrNull { (_, data) -> data.exactMatch != -1 }
?.second?.let { data -> data.packages.getOrNull(data.exactMatch)?.name }
exactMatchPackageName?.let { packageName ->
val installedPackageIndex = installedPackages.items.indexOfFirst { it.name == packageName }
if (installedPackageIndex != -1) {
installedPackages.tree.tree.setSelectionInterval(installedPackageIndex, installedPackageIndex)
installedPackages.tree.table.setRowSelectionInterval(installedPackageIndex, installedPackageIndex)
}
}
}
fun resetSearch(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>, currentSdk: Sdk?) {
updatePackages(installed, repoData)
installedPackages.expand()
installedPackages.setSdkToHeader(currentSdk?.name)
repositories.forEach {
it.collapseAll()
it.updateHeaderText(null)
}
container.scrollRectToVisible(Rectangle(0, 0))
}
private fun updatePackages(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) {
val sortedInstalled = installed.sortedBy { it.name }
installedPackages.tree.items = sortedInstalled
updateExistingRepository(installedPackages, sortedInstalled)
val (validRepoData, invalidData) = repoData.partition { it !is PyInvalidRepositoryViewData }
updateValidRepositories(validRepoData)
cleanupRemovedRepositories(validRepoData.map { it.repository.name })
val invalidRepoData = invalidData.filterIsInstance<PyInvalidRepositoryViewData>()
refreshInvalidRepositories(invalidRepoData)
}
private fun synchronizeScrollPaneSize() {
getRepos().forEach { repo ->
repo.updatePreferredSize()
}
container.revalidate()
container.repaint()
}
private fun updateValidRepositories(validRepoData: List<PyPackagesViewData>) {
for (data in validRepoData) {
val withExpander = if (data.moreItems > 0) {
data.packages + listOf(ExpandResultNode(data.moreItems, data.repository))
}
else {
data.packages
}
val existingRepo = findTableForRepo(data.repository)
if (existingRepo != null) {
updateExistingRepository(existingRepo, withExpander)
}
else {
createNewRepository(data.repository, withExpander)
}
}
}
private fun updateExistingRepository(repo: PyPackagingTreeGroup, items: List<DisplayablePackage>) {
repo.removeFrom(container)
val selectedPackage = repo.items.firstOrNull { it in repo.tree.items }
repo.items = items
repo.addTo(container)
if (selectedPackage != null) {
repo.tree.items = items
}
synchronizeScrollPaneSize()
}
private fun createNewRepository(repository: PyPackageRepository, items: List<DisplayablePackage>) {
val newTable = PyPackagesTreeTable(project, controller)
newTable.items = items
val newTableGroup = PyPackagingTreeGroup(repository, newTable, container)
newTableGroup.items = items
repositories.add(newTableGroup)
newTableGroup.addTo(container)
newTable.tree.addTreeSelectionListener {
syncTreeSelection(newTable)
}
synchronizeScrollPaneSize()
}
private fun cleanupRemovedRepositories(existingRepositoryNames: List<String>) {
val removedRepositories = repositories.filter { it.repositoryName !in existingRepositoryNames }
removedRepositories.forEach { it.removeFrom(container) }
repositories.removeAll(removedRepositories)
}
private fun refreshInvalidRepositories(invalid: List<PyInvalidRepositoryViewData>) {
val invalidRepoNames = invalid.map { it.repository.name }
invalidRepositories.forEach { container.remove(it.value) }
invalidRepositories.keys.removeIf { it !in invalidRepoNames }
invalid.asSequence()
.map { it.repository.name }
.filterNot { it in invalidRepositories }
.map {
val label = JLabel(message("python.toolwindow.packages.custom.repo.invalid", it)).apply {
foreground = JBColor.RED
icon = AllIcons.General.Error
}
it to PyPackagesUiComponents.headerPanel(label, null)
}
.forEach {
invalidRepositories[it.first] = it.second
}
invalidRepositories.forEach { container.add(it.value) }
}
fun selectPackage(packageName: String) {
val repos = getRepos()
for (repo in repos) {
val pkg = repo.items.firstOrNull { it.name == packageName } ?: continue
repo.tree.selectPackage(pkg)
}
}
private fun findTableForRepo(repository: PyPackageRepository) = repositories.find { it.repositoryName == repository.name }
fun getSelectedPackages(): List<DisplayablePackage> {
val repos = getRepos()
return repos.flatMap { it.tree.selectedItems() }
}
fun collapseAll() {
getRepos().forEach { it.collapseAll() }
}
private fun getRepos() = listOf(installedPackages) + repositories
internal fun syncTreeSelection(selectedTree: PyPackagesTreeTable) {
if (isSyncingTreeSelection) return
try {
isSyncingTreeSelection = true
getRepos().map { it.tree }.filter { it != selectedTree }.forEach {
it.tree.clearSelection()
it.table.clearSelection()
}
} finally {
isSyncingTreeSelection = false
}
}
}
@@ -3,8 +3,10 @@ package com.jetbrains.python.packaging.toolwindow.actions
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAwareAction
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackages
@@ -20,9 +22,12 @@ internal class UpdatePackageToLatestAction : DumbAwareAction() {
return
}
val service = PyPackagingToolWindowService.getInstance(project)
PyPackageCoroutine.getIoScope(project).launch {
service.updatePackages(*packages.map { it.name }.toTypedArray())
val pyPackages = packages.mapNotNull { pkg ->
pkg.repository?.findPackageSpecification(pkg.name, pkg.nextVersion?.presentableText)
}
val installRequest = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(pyPackages)
project.service<PyPackagingToolWindowService>().installPackage(installRequest)
}
}
@@ -33,9 +33,7 @@ import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.management.toInstallRequest
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.actions.InstallWithOptionsPackageAction
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.model.*
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import kotlinx.coroutines.Dispatchers
@@ -142,7 +140,14 @@ class PyPackageDescriptionController(val project: Project) : Disposable {
}
val comboBox = cell(versionSelector)
comboBox.enabledIf(isManagement.and(progressEnabledProperty.not())).gap(RightGap.SMALL)
comboBox.visibleIf(progressEnabledProperty.not())
comboBox.visibleIf(progressEnabledProperty.not().and(
selectedPackage.transform { pkg ->
when (pkg) {
is InstallablePackage, is InstalledPackage -> true
is RequirementPackage, is ExpandResultNode, is DisplayablePackage, null -> false
}
}
))
installActionButton.action = installAction
installActionButton.options = arrayOf(installWithOptionAction)
@@ -2,40 +2,37 @@
package com.jetbrains.python.packaging.toolwindow.model
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.packaging.PyPackageVersion
import com.jetbrains.python.packaging.PyPackageVersionComparator
import com.jetbrains.python.packaging.PyPackageVersionNormalizer
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.conda.CondaPackage
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.psi.icons.PythonPsiApiIcons
import javax.swing.Icon
sealed class DisplayablePackage(val name: @NlsSafe String, open val repository: PyPackageRepository?)
sealed class DisplayablePackage(val name: @NlsSafe String, open val repository: PyPackageRepository?) {
open fun getRequirements(): List<RequirementPackage> = emptyList()
}
class InstalledPackage(val instance: PythonPackage, repository: PyPackageRepository?, val nextVersion: PyPackageVersion? = null) : DisplayablePackage(instance.presentableName, repository) {
class InstalledPackage(val instance: PythonPackage, repository: PyPackageRepository?, val nextVersion: PyPackageVersion? = null, private val requirements: List<RequirementPackage>) : DisplayablePackage(instance.name, repository) {
val currentVersion: PyPackageVersion? = PyPackageVersionNormalizer.normalize(instance.version)
val isEditMode: Boolean = instance.isEditableMode
val sourceRepoIcon: Icon?
get() {
val condaPackage = instance as? CondaPackage ?: return null
return if (condaPackage.installedWithPip) {
PythonPsiApiIcons.Python
}
else {
PythonIcons.Python.Anaconda
}
}
val sourceRepoIcon: Icon = instance.sourceRepoIcon
val canBeUpdated: Boolean
get() {
currentVersion ?: return false
return nextVersion != null && PyPackageVersionComparator.compare(nextVersion, currentVersion) > 0
}
override fun getRequirements(): List<RequirementPackage> = requirements
}
class RequirementPackage(val instance: PythonPackage, override val repository: PyPackageRepository, private val requirements: List<RequirementPackage> = emptyList()) : DisplayablePackage(instance.name, repository) {
val sourceRepoIcon: Icon = instance.sourceRepoIcon
override fun getRequirements(): List<RequirementPackage> = requirements
}
class InstallablePackage(name: String, override val repository: PyPackageRepository) : DisplayablePackage(name, repository)
@@ -11,25 +11,29 @@ import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.JBPanelWithEmptyText
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.toolwindow.PyPackagingTablesView
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowPanel
import com.jetbrains.python.packaging.toolwindow.PyPackagingTreeView
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.model.PyPackagesViewData
import java.awt.BorderLayout
import javax.swing.BoxLayout
import javax.swing.JPanel
import javax.swing.JScrollPane
import javax.swing.ScrollPaneConstants
class PyPackagesListController(val project: Project, val controller: PyPackagingToolWindowPanel) : Disposable {
internal class PyPackagesListController(val project: Project, val controller: PyPackagingToolWindowPanel) : Disposable {
private val packageListPanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
alignmentX = LEFT_ALIGNMENT
background = UIUtil.getListBackground()
}
private val tablesView = PyPackagingTablesView(project, packageListPanel, controller)
private val tablesView = PyPackagingTreeView(project, packageListPanel, controller)
private val scrollingPackageListComponent = ScrollPaneFactory.createScrollPane(packageListPanel, true)
private val scrollingPackageListComponent: JScrollPane = ScrollPaneFactory.createScrollPane(packageListPanel, true).apply {
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
}
private val loadingPanel = JBPanelWithEmptyText().apply {
emptyText.appendLine(AnimatedIcon.Default.INSTANCE, message("python.toolwindow.packages.description.panel.loading"), SimpleTextAttributes.SIMPLE_CELL_ATTRIBUTES, null)
@@ -72,7 +76,7 @@ class PyPackagesListController(val project: Project, val controller: PyPackaging
tablesView.collapseAll()
}
internal fun setLoadingState(isLoading: Boolean) {
internal fun setLoadingState(isLoading: Boolean) {
val newPanel = if (isLoading) loadingPanel else scrollingPackageListComponent
val currentComponent = component.components.firstOrNull()
@@ -1,69 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.toolwindow.packages
import com.intellij.icons.AllIcons
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.components.JBLabel
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.packages.table.PyPackagesTable
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import org.jetbrains.annotations.Nls
import javax.swing.JPanel
internal class PyPackagingTableGroup(val repository: PyPackageRepository, val table: PyPackagesTable) {
@NlsSafe
val name: String = repository.name!!
private var expanded = false
private val label = JBLabel(name).apply { icon = AllIcons.General.ArrowDown }
private val header: JPanel = PyPackagesUiComponents.headerPanel(label, table)
private var itemsCount: Int? = null
internal var items: List<DisplayablePackage>
get() = table.items
set(value) {
table.items = value
}
fun collapse() {
expanded = false
table.isVisible = false
label.icon = if (table.isVisible) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
}
fun expand() {
expanded = true
table.isVisible = true
label.icon = if (table.isVisible) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
}
fun updateHeaderText(newItemCount: Int?) {
itemsCount = newItemCount
label.text = if (itemsCount == null) name else PyBundle.message("python.toolwindow.packages.custom.repo.searched", name, itemsCount)
}
fun setSdkToHeader(@Nls sdkName: String?) {
itemsCount = null
@Suppress("HardCodedStringLiteral")
label.text = "<html>$name <b>(${sdkName})</b></html>"
}
fun addTo(panel: JPanel) {
panel.add(header)
panel.add(table)
}
fun removeFrom(panel: JPanel) {
panel.remove(header)
panel.remove(table)
}
fun repaint() {
table.invalidate()
table.repaint()
}
}
@@ -0,0 +1,146 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.toolwindow.packages
import com.intellij.icons.AllIcons
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.packages.tree.PyPackagesTreeListener
import com.jetbrains.python.packaging.toolwindow.packages.tree.PyPackagesTreeTable
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import org.jetbrains.annotations.Nls
import java.awt.Component
import java.awt.Dimension
import javax.swing.JPanel
import javax.swing.ScrollPaneConstants
import javax.swing.SwingConstants
import javax.swing.SwingUtilities
internal class PyPackagingTreeGroup(
repository: PyPackageRepository,
val tree: PyPackagesTreeTable,
private val container: JPanel,
) {
private data class HeaderProperties(
val label: JBLabel,
val panel: JPanel,
)
@NlsSafe
val repositoryName: String = repository.name
private var itemsCount: Int? = null
val scrollPane = JBScrollPane(tree).apply {
border = JBUI.Borders.empty()
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
}
private val headerProperties: HeaderProperties = createHeaderProperties()
internal var items: List<DisplayablePackage>
get() = tree.items
set(value) {
tree.items = value
}
init {
setupTreeListener()
}
private fun createHeaderProperties(): HeaderProperties {
val label = JBLabel(repositoryName).apply {
icon = AllIcons.General.ArrowDown
horizontalAlignment = SwingConstants.LEFT
verticalAlignment = SwingConstants.CENTER
}
val panel = PyPackagesUiComponents.headerPanel(label, scrollPane)
return HeaderProperties(label, panel)
}
private fun setupTreeListener() {
tree.setTreeListener(object : PyPackagesTreeListener {
override fun onTreeStructureChanged() {
SwingUtilities.invokeLater {
updatePreferredSize()
repaint()
}
}
})
}
fun setItems(value: List<DisplayablePackage>) {
tree.items = value
}
fun updatePreferredSize() {
val totalHeight = tree.tree.rowCount * tree.tree.rowHeight
val width = container.width
scrollPane.preferredSize = Dimension(width, totalHeight)
scrollPane.minimumSize = Dimension(width, totalHeight)
scrollPane.maximumSize = Dimension(width, totalHeight)
}
fun collapse() {
scrollPane.isVisible = false
updateExpandStateIcon()
repaint()
}
fun collapseAll() {
for (i in 0 until tree.tree.rowCount) {
tree.tree.collapseRow(i)
}
collapse()
}
fun expand() {
scrollPane.isVisible = true
updateExpandStateIcon()
repaint()
}
private fun updateExpandStateIcon() {
headerProperties.label.icon = if (scrollPane.isVisible)
AllIcons.General.ArrowDown
else
AllIcons.General.ArrowRight
}
fun updateHeaderText(newItemCount: Int?) {
itemsCount = newItemCount
headerProperties.label.text = if (itemsCount == null)
repositoryName
else
PyBundle.message("python.toolwindow.packages.custom.repo.searched", repositoryName, itemsCount)
}
fun setSdkToHeader(@Nls sdkName: String?) {
itemsCount = null
headerProperties.label.text = PyBundle.message("python.toolwindow.packages.sdk.label.html", repositoryName, sdkName)
}
fun addTo(panel: JPanel) {
scrollPane.alignmentX = Component.LEFT_ALIGNMENT
panel.add(headerProperties.panel)
panel.add(scrollPane)
}
fun removeFrom(panel: JPanel) {
panel.remove(headerProperties.panel)
panel.remove(scrollPane)
}
fun repaint() {
container.revalidate()
container.repaint()
}
}
@@ -1,122 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.toolwindow.packages
import com.intellij.icons.AllIcons
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.components.JBLabel
import com.intellij.ui.hover.TableHoverListener
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.model.ExpandResultNode
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.packages.table.PyPackagesTable
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import java.awt.Component
import java.awt.font.TextAttribute
import javax.swing.BoxLayout
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JTable
import javax.swing.table.DefaultTableCellRenderer
internal class PyPaginationAwareRenderer : DefaultTableCellRenderer() {
private val nameLabel = JBLabel().apply { border = JBUI.Borders.empty(0, 12) }
private val versionLabel = JLabel().apply { border = JBUI.Borders.emptyRight(12) }
private val linkLabel = JLabel(PyBundle.message("action.python.packages.install.text")).apply {
border = JBUI.Borders.emptyRight(12)
foreground = JBUI.CurrentTheme.Link.Foreground.ENABLED
}
private val namePanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.X_AXIS)
border = JBUI.Borders.empty()
add(nameLabel)
}
private val versionPanel = PyPackagesUiComponents.boxPanel {
border = JBUI.Borders.emptyRight(12)
add(versionLabel)
}
override fun getTableCellRendererComponent(
table: JTable,
value: Any?,
isSelected: Boolean,
hasFocus: Boolean,
row: Int,
column: Int,
): Component {
val rowSelected = row in table.selectedRows
val tableFocused = table.hasFocus()
if (value is ExpandResultNode) {
if (column == 1) {
versionPanel.removeAll()
return versionPanel
}
else {
nameLabel.text = PyBundle.message("python.toolwindow.packages.load.more", value.more)
nameLabel.foreground = UIUtil.getContextHelpForeground()
return namePanel
}
}
// version column
if (column == 1) {
versionPanel.background = JBUI.CurrentTheme.Table.background(rowSelected, tableFocused)
versionPanel.foreground = JBUI.CurrentTheme.Table.foreground(rowSelected, tableFocused)
versionPanel.removeAll()
if (value is InstallablePackage) {
linkLabel.text = PyBundle.message("action.python.packages.install.text")
linkLabel.updateUnderline(table, row)
if (rowSelected || TableHoverListener.getHoveredRow(table) == row) {
versionPanel.add(linkLabel)
}
}
else if (value is InstalledPackage && value.nextVersion != null && value.canBeUpdated) {
@NlsSafe val updateLink = value.instance.version + " -> " + value.nextVersion.presentableText
linkLabel.text = updateLink
linkLabel.updateUnderline(table, row)
versionPanel.add(linkLabel)
}
else {
@NlsSafe val version = (value as InstalledPackage).instance.version
versionLabel.text = version
versionPanel.add(versionLabel)
}
versionLabel.icon = (value as? InstalledPackage)?.sourceRepoIcon
return versionPanel
}
// package name column
val currentPackage = value as DisplayablePackage
namePanel.background = JBUI.CurrentTheme.Table.background(rowSelected, tableFocused)
namePanel.foreground = JBUI.CurrentTheme.Table.foreground(rowSelected, tableFocused)
nameLabel.text = currentPackage.name
val isEditMode = (currentPackage as? InstalledPackage)?.isEditMode == true
nameLabel.icon = if (isEditMode) AllIcons.Actions.Edit else null
nameLabel.foreground = JBUI.CurrentTheme.Label.foreground()
return namePanel
}
@Suppress("UNCHECKED_CAST")
private fun JLabel.updateUnderline(table: JTable, currentRow: Int) {
val hoveredRow = TableHoverListener.getHoveredRow(table)
val hoveredColumn = (table as PyPackagesTable).hoveredColumn
val underline = if (hoveredRow == currentRow && hoveredColumn == 1) TextAttribute.UNDERLINE_ON else -1
val attributes = font.attributes as MutableMap<TextAttribute, Any>
attributes[TextAttribute.UNDERLINE] = underline
attributes[TextAttribute.LIGATURES] = TextAttribute.LIGATURES_ON
font = font.deriveFont(attributes)
}
}
@@ -1,57 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.toolwindow.packages.table
import com.intellij.codeInsight.hints.presentation.MouseButton
import com.intellij.codeInsight.hints.presentation.mouseButton
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.hover.TableHoverListener
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
internal class PyPackageTableMouseAdapter(private val table: PyPackagesTable) : MouseAdapter() {
val project: Project = table.project
val service
get() = project.service<PyPackagingToolWindowService>()
override fun mouseClicked(e: MouseEvent) {
// double click or click on package name column, nothing to be done
if (e.mouseButton != MouseButton.Left ||
e.clickCount != 1 ||
table.columnAtPoint(e.point) != 1) {
return
}
val hoveredRow = TableHoverListener.getHoveredRow(table)
val selectedPackage = table.items.getOrNull(hoveredRow) ?: table.selectedItem() ?: return
if (selectedPackage is InstallablePackage) {
PyPackageCoroutine.launch(project) {
val details = service.detailsForPackage(selectedPackage) ?: return@launch
withContext(Dispatchers.EDT) {
PyPackagesUiComponents.createAvailableVersionsPopup(selectedPackage, details, project).show(RelativePoint(e))
}
}
return
}
if (selectedPackage is InstalledPackage &&
selectedPackage.canBeUpdated &&
selectedPackage.repository != null &&
selectedPackage.nextVersion != null) {
PyPackageCoroutine.launch(project) {
project.service<PyPackagingToolWindowService>().updatePackages(selectedPackage.name)
}
}
}
}
@@ -1,25 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.toolwindow.packages.table
import com.intellij.ui.hover.TableHoverListener
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import java.awt.Cursor
import javax.swing.JTable
internal class PyPackagesHoverListener(private val pyPackageTable: PyPackagesTable) : TableHoverListener() {
override fun onHover(table: JTable, row: Int, column: Int) {
pyPackageTable.hoveredColumn = column
if (column == 1) {
table.repaint(table.getCellRect(row, column, true))
val currentPackage = pyPackageTable.items[row]
if (currentPackage is InstallablePackage
|| (currentPackage is InstalledPackage && currentPackage.canBeUpdated)) {
table.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
return
}
}
table.cursor = Cursor.getDefaultCursor()
}
}
@@ -1,162 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.toolwindow.packages.table
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.ui.DoubleClickListener
import com.intellij.ui.PopupHandler
import com.intellij.ui.SideBorder
import com.intellij.ui.table.JBTable
import com.intellij.util.ui.NamedColorUtil
import com.jetbrains.python.packaging.toolwindow.PyPackagingTablesView
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowPanel
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.model.ExpandResultNode
import com.jetbrains.python.packaging.toolwindow.packages.PyPaginationAwareRenderer
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesTableModel
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
import javax.swing.AbstractAction
import javax.swing.KeyStroke
import javax.swing.ListSelectionModel
@Suppress("UsePropertyAccessSyntax")
internal class PyPackagesTable(
val project: Project,
tablesView: PyPackagingTablesView,
val controller: PyPackagingToolWindowPanel,
) : JBTable(PyPackagesTableModel<DisplayablePackage>()) {
internal var hoveredColumn = -1
@Suppress("UNCHECKED_CAST")
val model: PyPackagesTableModel<DisplayablePackage> = getModel() as PyPackagesTableModel<DisplayablePackage>
var items: List<DisplayablePackage>
get() = model.items
set(value) {
model.items = value.toMutableList()
}
init {
val service = project.service<PyPackagingToolWindowService>()
setShowGrid(false)
setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
val column = columnModel.getColumn(1)
column.minWidth = 130
column.maxWidth = 130
column.resizable = false
border = SideBorder(NamedColorUtil.getBoundsColor(), SideBorder.BOTTOM)
rowHeight = 20
initCrossNavigation(service, tablesView)
val hoverListener = PyPackagesHoverListener(this)
hoverListener.addTo(this)
addMouseListener(PyPackageTableMouseAdapter(this))
selectionModel.addListSelectionListener {
val pkg = selectedItem()
if (pkg != null && pkg !is ExpandResultNode) {
tablesView.removeSelectionNotFormTable(this)
controller.packageSelected(pkg)
}
else {
controller.setEmpty()
}
}
object : DoubleClickListener() {
override fun onDoubleClick(event: MouseEvent): Boolean {
val pkg = selectedItem() ?: return true
if (pkg is ExpandResultNode)
loadMoreItems(service, pkg)
return true
}
}.installOn(this)
val packageActionGroup = ActionManager.getInstance().getAction("PyPackageToolwindowContext") as ActionGroup
PopupHandler.installPopupMenu(this, packageActionGroup, "PackagePopup")
}
override fun getCellRenderer(row: Int, column: Int) = PyPaginationAwareRenderer()
fun selectedItem(): DisplayablePackage? = items.getOrNull(selectedRow)
fun selectedItems(): Sequence<DisplayablePackage> {
return selectedRows.asSequence().mapNotNull { items.getOrNull(it) }
}
fun selectPackage(pkg: DisplayablePackage) {
val index = items.indexOf(pkg)
if (index != -1) {
setRowSelectionInterval(index, index)
}
}
private fun initCrossNavigation(service: PyPackagingToolWindowService, tablesView: PyPackagingTablesView) {
getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ENTER_ACTION)
actionMap.put(ENTER_ACTION, object : AbstractAction() {
override fun actionPerformed(e: ActionEvent?) {
if (selectedRow == -1) return
val index = selectedRow
val selectedItem = selectedItem() ?: return
if (selectedItem is ExpandResultNode) {
loadMoreItems(service, selectedItem)
}
setRowSelectionInterval(index, index)
}
})
val nextRowAction = actionMap[NEXT_ROW_ACTION]
actionMap.put(NEXT_ROW_ACTION, object : AbstractAction() {
override fun actionPerformed(e: ActionEvent?) {
if (selectedRow == -1) return
if (selectedRow + 1 == items.size) {
tablesView.selectNextFrom(this@PyPackagesTable)
}
else {
nextRowAction.actionPerformed(e)
}
}
})
val prevRowAction = actionMap[PREVIOUS_ROW_ACTION]
actionMap.put(PREVIOUS_ROW_ACTION, object : AbstractAction() {
override fun actionPerformed(e: ActionEvent?) {
if (selectedRow == -1) return
if (selectedRow == 0) {
tablesView.selectPreviousOf(this@PyPackagesTable)
}
else {
prevRowAction.actionPerformed(e)
}
}
})
}
private fun loadMoreItems(service: PyPackagingToolWindowService, node: ExpandResultNode) {
val result = service.getMoreResultsForRepo(node.repository, items.size - 1) ?: return
items = items.dropLast(1) + result.packages
if (result.moreItems > 0) {
node.more = result.moreItems
items = items + listOf(node)
}
this@PyPackagesTable.revalidate()
this@PyPackagesTable.repaint()
}
companion object {
private const val NEXT_ROW_ACTION = "selectNextRow"
private const val PREVIOUS_ROW_ACTION = "selectPreviousRow"
private const val ENTER_ACTION = "ENTER"
}
}
@@ -0,0 +1,85 @@
// 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.packaging.toolwindow.packages.tree
import com.intellij.codeInsight.hints.presentation.MouseButton
import com.intellij.codeInsight.hints.presentation.mouseButton
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.ui.awt.RelativePoint
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
private interface PackageHandler {
suspend fun handle(treeTable: PyPackagesTreeTable, pkg: DisplayablePackage, event: MouseEvent)
companion object {
fun forPackage(pkg: DisplayablePackage): PackageHandler? = when (pkg) {
is InstallablePackage -> InstallablePackageHandler()
is InstalledPackage -> InstalledPackageHandler()
else -> null
}
}
}
private class InstallablePackageHandler : PackageHandler {
override suspend fun handle(treeTable: PyPackagesTreeTable, pkg: DisplayablePackage, event: MouseEvent) {
val project: Project = treeTable.project
val packagingService = project.service<PyPackagingToolWindowService>()
val details = packagingService.detailsForPackage(pkg) ?: return
withContext(Dispatchers.Main) {
PyPackagesUiComponents.createAvailableVersionsPopup(pkg, details, project)
.show(RelativePoint(event))
treeTable.tree.requestFocus()
}
}
}
private class InstalledPackageHandler : PackageHandler {
override suspend fun handle( treeTable: PyPackagesTreeTable, pkg: DisplayablePackage, event: MouseEvent) {
val project: Project = treeTable.project
val packagingService = project.service<PyPackagingToolWindowService>()
if (pkg !is InstalledPackage || !pkg.canBeUpdated) return
pkg.nextVersion?.let { version ->
val pkgToUpdate = PythonPackage(pkg.name, version.presentableText, false)
packagingService.installPackage(pkgToUpdate)
}
}
}
internal class PyPackageTableMouseAdapter(private val treeTable: PyPackagesTreeTable) : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
if (!isValidLeftClick(e)) return
val selectedPackage = findTargetPackageAtPoint(e) ?: return
PyPackageCoroutine.launch(treeTable.project, Dispatchers.IO) {
PackageHandler.forPackage (selectedPackage)?.handle(treeTable, selectedPackage, e)
}
}
private fun isValidLeftClick(e: MouseEvent): Boolean =
e.mouseButton == MouseButton.Left && e.clickCount == SINGLE_CLICK
private fun findTargetPackageAtPoint(e: MouseEvent): DisplayablePackage? {
if (e.source != treeTable.table) return null
val row = treeTable.table.rowAtPoint(e.point)
return if (row >= 0) treeTable.items.getOrNull(row) else null
}
companion object {
private const val SINGLE_CLICK = 1
}
}
@@ -0,0 +1,195 @@
// 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.packaging.toolwindow.packages.tree
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.putUserData
import com.intellij.openapi.util.Key
import com.intellij.ui.DoubleClickListener
import com.intellij.ui.PopupHandler
import com.intellij.ui.components.JBTreeTable
import com.intellij.ui.hover.TableHoverListener
import com.intellij.ui.hover.TreeHoverListener
import com.intellij.ui.treeStructure.treetable.TreeTableModel
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowPanel
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.model.ExpandResultNode
import com.jetbrains.python.packaging.toolwindow.packages.tree.renderers.PackageNameCellRenderer
import com.jetbrains.python.packaging.toolwindow.packages.tree.renderers.PackageVersionCellRenderer
import org.jetbrains.annotations.ApiStatus
import java.awt.event.MouseEvent
import javax.swing.JScrollPane
import javax.swing.JTable
import javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
import javax.swing.event.TreeExpansionEvent
import javax.swing.event.TreeExpansionListener
import javax.swing.event.TreeSelectionListener
import javax.swing.tree.TreeSelectionModel.SINGLE_TREE_SELECTION
@ApiStatus.Internal
class PyPackagesTreeTable(
val project: Project,
private val controller: PyPackagingToolWindowPanel,
private var treeListener: PyPackagesTreeListener? = null,
) : JBTreeTable(PyPackagesTreeTableModel()), PackageTreeTableOperations {
companion object {
private const val COLUMN_PROPORTION = 0.3f
private const val POPUP_MENU_PLACE = "PackagePopup"
private const val PACKAGE_ACTION_GROUP_ID = "PyPackageToolwindowContext"
private const val INVALID_POSITION = -1
internal val TREE_TABLE_KEY: Key<PyPackagesTreeTable> = Key.create("PyPackageToolwindow.TreeTable")
}
private val treeTableModel: PyPackagesTreeTableModel
get() = model as PyPackagesTreeTableModel
private val packagingService = project.service<PyPackagingToolWindowService>()
var hoveredColumn: Int = INVALID_POSITION
var items: List<DisplayablePackage> = emptyList()
set(value) {
field = value
treeTableModel.items = value
}
init {
table.putUserData(TREE_TABLE_KEY, this)
initializeUI()
}
private fun initializeUI() {
initializeTreeTableProperties()
initializeTreeProperties()
initializeCellRenderers()
setupTreeInteractions()
setupContextMenu()
}
private fun initializeTreeTableProperties() {
splitter.setResizeEnabled(false)
val secondComponent = splitter.secondComponent as JScrollPane
secondComponent.apply {
horizontalScrollBarPolicy = HORIZONTAL_SCROLLBAR_NEVER
}
setColumnProportion(COLUMN_PROPORTION)
}
private fun initializeTreeProperties() {
tree.apply {
isRootVisible = false
showsRootHandles = true
selectionModel.selectionMode = SINGLE_TREE_SELECTION
}
}
private fun initializeCellRenderers() {
setDefaultRenderer(TreeTableModel::class.java, PackageNameCellRenderer())
setDefaultRenderer(DisplayablePackage::class.java, PackageVersionCellRenderer())
}
private fun setupTreeInteractions() {
setupTreeEventListeners()
setupMouseAndHoverHandlers()
}
private fun setupTreeEventListeners() {
tree.addTreeSelectionListener(createPackageSelectionListener())
tree.addTreeExpansionListener(createTreeExpansionListener())
installPackageDoubleClickHandler()
}
private fun createPackageSelectionListener() = TreeSelectionListener { event ->
val path = event.path ?: return@TreeSelectionListener
val node = path.lastPathComponent
when (val pkg = treeTableModel.getValueAt(node, 0)) {
is DisplayablePackage -> controller.packageSelected(pkg)
else -> controller.setEmpty()
}
}
private fun createTreeExpansionListener() = object : TreeExpansionListener {
override fun treeExpanded(event: TreeExpansionEvent) {
treeListener?.onTreeStructureChanged()
}
override fun treeCollapsed(event: TreeExpansionEvent) {
treeListener?.onTreeStructureChanged()
}
}
private fun setupMouseAndHoverHandlers() {
val mouseHandler = PyPackageTableMouseAdapter(this)
tree.addMouseListener(mouseHandler)
table.addMouseListener(mouseHandler)
setupHoverTracking()
}
private fun setupHoverTracking() {
TreeHoverListener.DEFAULT.addTo(tree)
TableHoverListener.DEFAULT.addTo(table)
object : TableHoverListener() {
override fun onHover(table: JTable, row: Int, column: Int) {
hoveredColumn = column
}
}.addTo(table)
}
private fun installPackageDoubleClickHandler() {
object : DoubleClickListener() {
override fun onDoubleClick(event: MouseEvent): Boolean {
val pkg = selectedItem() as? ExpandResultNode ?: return true
loadMoreItems(pkg)
return true
}
}.installOn(tree)
}
private fun setupContextMenu() {
val packageActionGroup = ActionManager.getInstance()
.getAction(PACKAGE_ACTION_GROUP_ID) as ActionGroup
PopupHandler.installPopupMenu(tree, packageActionGroup, POPUP_MENU_PLACE)
PopupHandler.installPopupMenu(table, packageActionGroup, POPUP_MENU_PLACE)
}
private fun loadMoreItems(node: ExpandResultNode) {
val result = packagingService.getMoreResultsForRepo(node.repository, items.size - 1) ?: return
items = items.dropLast(1) + result.packages
if (result.moreItems > 0) {
node.more = result.moreItems
items = items + listOf(node)
}
}
override fun setTreeListener(listener: PyPackagesTreeListener) {
treeListener = listener
}
override fun selectPackage(pkg: DisplayablePackage) {
val index = items.indexOf(pkg)
if (index != -1) {
tree.setSelectionRow(index)
}
}
override fun selectedItems(): Sequence<DisplayablePackage> =
tree.selectionRows?.asSequence()?.mapNotNull { row ->
val node = tree.getPathForRow(row)?.lastPathComponent ?: return@mapNotNull null
treeTableModel.getValueAt(node, 0) as? DisplayablePackage
} ?: emptySequence()
}
private interface PackageTreeTableOperations {
fun setTreeListener(listener: PyPackagesTreeListener)
fun selectPackage(pkg: DisplayablePackage)
fun selectedItems(): Sequence<DisplayablePackage>
fun selectedItem(): DisplayablePackage? = selectedItems().firstOrNull()
}
@ApiStatus.Internal
interface PyPackagesTreeListener {
fun onTreeStructureChanged()
}
@@ -0,0 +1,97 @@
// 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.packaging.toolwindow.packages.tree
import com.intellij.ui.treeStructure.treetable.TreeTableModel
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import javax.swing.JTree
import javax.swing.event.TreeModelListener
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreePath
/**
* TreeTableModel implementation for PyPackagesTree.
* Provides two columns: package name and package version.
*/
internal class PyPackagesTreeTableModel : TreeTableModel {
companion object {
private const val ROOT_NODE_NAME = "root"
private const val COLUMN_COUNT = 2
private const val NAME_COLUMN = 0
private const val VERSION_COLUMN = 1
}
private val rootNode = DefaultMutableTreeNode(ROOT_NODE_NAME)
private val treeModel = DefaultTreeModel(rootNode)
private var tree: JTree? = null
var items: List<DisplayablePackage> = emptyList()
set(value) {
field = value
updateTreeData(value)
}
override fun getColumnCount(): Int = COLUMN_COUNT
override fun getColumnName(column: Int): String = when (column) {
NAME_COLUMN -> ""
VERSION_COLUMN -> ""
else -> ""
}
override fun getColumnClass(column: Int): Class<*> = when (column) {
NAME_COLUMN -> TreeTableModel::class.java
VERSION_COLUMN -> DisplayablePackage::class.java
else -> Any::class.java
}
override fun getValueAt(node: Any, column: Int): Any? {
val treeNode = node as? DefaultMutableTreeNode ?: return null
val pkg = treeNode.userObject as? DisplayablePackage ?: return null
return when (column) {
NAME_COLUMN -> pkg
VERSION_COLUMN -> pkg
else -> null
}
}
override fun isCellEditable(node: Any, column: Int): Boolean = false
override fun setValueAt(aValue: Any?, node: Any, column: Int) {
error("unsupported")
}
override fun getChild(parent: Any, index: Int): Any? = treeModel.getChild(parent, index)
override fun getChildCount(parent: Any): Int = treeModel.getChildCount(parent)
override fun getIndexOfChild(parent: Any, child: Any): Int = treeModel.getIndexOfChild(parent, child)
override fun getRoot(): Any = treeModel.root
override fun isLeaf(node: Any): Boolean = treeModel.isLeaf(node)
override fun valueForPathChanged(path: TreePath, newValue: Any) = treeModel.valueForPathChanged(path, newValue)
override fun addTreeModelListener(l: TreeModelListener) = treeModel.addTreeModelListener(l)
override fun removeTreeModelListener(l: TreeModelListener) = treeModel.removeTreeModelListener(l)
override fun setTree(tree: JTree) {
this.tree = tree
}
private fun updateTreeData(packages: List<DisplayablePackage>) {
rootNode.removeAllChildren()
packages.forEach { pkg ->
rootNode.add(createNodeRecursively(pkg))
}
treeModel.reload()
}
private fun createNodeRecursively(
pkg: DisplayablePackage,
): DefaultMutableTreeNode {
val node = DefaultMutableTreeNode(pkg)
pkg.getRequirements().forEach { requirement ->
node.add(createNodeRecursively(requirement))
}
return node
}
}
@@ -0,0 +1,27 @@
// 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.packaging.toolwindow.packages.tree.renderers
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import java.awt.Color
import javax.swing.BoxLayout
import javax.swing.JPanel
import javax.swing.tree.DefaultMutableTreeNode
internal object PackageRendererUtils {
fun createBasicPanel(): JPanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.X_AXIS)
isOpaque = false
}
fun getBackgroundForState(isSelected: Boolean): Color = when {
isSelected -> UIUtil.getTreeSelectionBackground(true)
else -> UIUtil.getTreeBackground()
}
fun extractPackage(value: Any?): DisplayablePackage? = when (value) {
is DisplayablePackage -> value
is DefaultMutableTreeNode -> value.userObject as? DisplayablePackage
else -> null
}
}
@@ -0,0 +1,81 @@
// 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.packaging.toolwindow.packages.tree.renderers
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.hover.TableHoverListener
import com.intellij.ui.hover.TreeHoverListener
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPackageVersion
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.model.RequirementPackage
import com.jetbrains.python.packaging.toolwindow.packages.tree.PyPackagesTreeTable
import java.awt.BorderLayout
import java.awt.font.TextAttribute
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JTable
internal fun installablePackageVersionStrategy(
versionPanel: JPanel,
tableTree: PyPackagesTreeTable,
row: Int,
linkLabel: JLabel,
) {
val hoveredRow = TreeHoverListener.getHoveredRow(tableTree.tree)
val isRowHovered = hoveredRow == row
val isSelected = tableTree.tree.isRowSelected(row)
if (isRowHovered || isSelected) {
linkLabel.text = PyBundle.message("action.python.packages.install.text")
linkLabel.updateUnderline(tableTree, tableTree.table, row)
versionPanel.add(linkLabel, BorderLayout.WEST)
}
}
internal fun updatableInstalledPackageStrategy(
versionPanel: JPanel,
pkg: InstalledPackage,
version: PyPackageVersion,
tableTree: PyPackagesTreeTable,
row: Int,
linkLabel: JLabel,
) {
@NlsSafe val updateLink = "${pkg.instance.version} -> ${version.presentableText}"
linkLabel.text = updateLink
linkLabel.updateUnderline(tableTree, tableTree.table, row)
versionPanel.add(linkLabel, BorderLayout.WEST)
}
internal fun requirementPackageStrategy(
versionPanel: JPanel,
pkg: RequirementPackage,
versionLabel: JLabel,
) {
@NlsSafe val version = pkg.instance.version
versionLabel.text = version
versionLabel.icon = pkg.sourceRepoIcon
versionPanel.add(versionLabel, BorderLayout.WEST)
}
internal fun defaultPackageStrategy(
versionPanel: JPanel,
pkg: InstalledPackage,
versionLabel: JLabel,
) {
@NlsSafe val version = pkg.instance.version
versionLabel.text = version
versionLabel.icon = pkg.sourceRepoIcon
versionPanel.add(versionLabel, BorderLayout.WEST)
}
private fun JLabel.updateUnderline(tableTree: PyPackagesTreeTable, table: JTable, currentRow: Int) {
val hoveredRow = TableHoverListener.getHoveredRow(table)
val hoveredColumn = tableTree.hoveredColumn
val underline = if (hoveredRow == currentRow && hoveredColumn == 0) TextAttribute.UNDERLINE_ON else -1
@Suppress("UNCHECKED_CAST")
val attributes = font.attributes as MutableMap<TextAttribute, Any>
attributes[TextAttribute.UNDERLINE] = underline
attributes[TextAttribute.LIGATURES] = TextAttribute.LIGATURES_ON
font = font.deriveFont(attributes)
}
@@ -0,0 +1,108 @@
// 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.packaging.toolwindow.packages.tree.renderers
import com.intellij.icons.AllIcons
import com.intellij.openapi.ui.getUserData
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.JBEmptyBorder
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.toolwindow.model.*
import com.jetbrains.python.packaging.toolwindow.packages.tree.PyPackagesTreeTable
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Component
import javax.swing.JLabel
import javax.swing.JTable
import javax.swing.table.TableCellRenderer
internal class PackageNameCellRenderer : TableCellRenderer {
private val nameLabel = JBLabel()
private val namePanel = PackageRendererUtils.createBasicPanel().apply { add(nameLabel) }
override fun getTableCellRendererComponent(
table: JTable,
value: Any?,
isSelected: Boolean,
hasFocus: Boolean,
row: Int,
column: Int,
): Component {
val pkg = PackageRendererUtils.extractPackage(value) ?: return JLabel()
val background = PackageRendererUtils.getBackgroundForState(isSelected)
return when (pkg) {
is ExpandResultNode -> createExpandNodeComponent(pkg, background)
else -> createNameComponent(pkg, background)
}
}
private fun createExpandNodeComponent(
node: ExpandResultNode,
bg: Color,
): Component {
val expandNodeLabel = JBLabel(PyBundle.message("python.toolwindow.packages.load.more", node.more)).apply {
foreground = UIUtil.getContextHelpForeground()
}
return PackageRendererUtils.createBasicPanel().apply {
add(expandNodeLabel)
background = bg
}
}
private fun createNameComponent(
pkg: DisplayablePackage,
bg: Color,
): Component = namePanel.apply {
nameLabel.text = pkg.name
nameLabel.icon = if (pkg is InstalledPackage && pkg.isEditMode) AllIcons.Actions.Edit else null
background = bg
}
}
internal class PackageVersionCellRenderer : TableCellRenderer {
private val versionLabel = JLabel().apply {
border = JBEmptyBorder(JBUI.CurrentTheme.ActionsList.cellPadding())
}
private val linkLabel = JLabel(PyBundle.message("action.python.packages.install.text")).apply {
foreground = JBUI.CurrentTheme.Link.Foreground.ENABLED
border = JBEmptyBorder(JBUI.CurrentTheme.ActionsList.cellPadding())
}
private val versionPanel = PyPackagesUiComponents.borderPanel {
add(versionLabel, BorderLayout.CENTER)
}
override fun getTableCellRendererComponent(
table: JTable,
value: Any?,
isSelected: Boolean,
hasFocus: Boolean,
row: Int,
column: Int,
): Component {
val pkg = PackageRendererUtils.extractPackage(value) ?: return JLabel()
val background = PackageRendererUtils.getBackgroundForState(isSelected)
val treeTable = table.getUserData(PyPackagesTreeTable.TREE_TABLE_KEY)!!
versionPanel.removeAll()
versionPanel.background = background
when (pkg) {
is InstallablePackage -> installablePackageVersionStrategy(versionPanel, treeTable, row, linkLabel)
is InstalledPackage -> if (pkg.nextVersion != null && pkg.canBeUpdated) {
updatableInstalledPackageStrategy(versionPanel, pkg, pkg.nextVersion, treeTable, row, linkLabel)
}
else {
defaultPackageStrategy(versionPanel, pkg, versionLabel)
}
is RequirementPackage -> requirementPackageStrategy(versionPanel, pkg, versionLabel)
is ExpandResultNode -> JLabel()
}
return versionPanel
}
}
@@ -1,13 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.toolwindow.ui
import com.intellij.util.ui.ListTableModel
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
internal class PyPackagesTableModel<T : DisplayablePackage> : ListTableModel<T>() {
override fun isCellEditable(rowIndex: Int, columnIndex: Int): Boolean = false
override fun getColumnCount(): Int = 2
override fun getColumnName(column: Int): String = column.toString()
override fun getColumnClass(columnIndex: Int): Class<*> = DisplayablePackage::class.java
override fun getValueAt(rowIndex: Int, columnIndex: Int): Any? = items[rowIndex]
}
@@ -14,6 +14,7 @@ import com.intellij.ui.SideBorder
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.NamedColorUtil
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.management.toInstallRequest
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
@@ -28,8 +29,8 @@ import java.awt.event.MouseEvent
import javax.swing.*
object PyPackagesUiComponents {
val SELECTED_PACKAGE_DATA_CONTEXT = DataKey.create<DisplayablePackage>("SELECTED_PACKAGE_DATA_CONTEXT")
val SELECTED_PACKAGES_DATA_CONTEXT = DataKey.create<List<DisplayablePackage>>("SELECTED_PACKAGES_DATA_CONTEXT")
val SELECTED_PACKAGE_DATA_CONTEXT: DataKey<DisplayablePackage> = DataKey.create<DisplayablePackage>("SELECTED_PACKAGE_DATA_CONTEXT")
val SELECTED_PACKAGES_DATA_CONTEXT: DataKey<List<DisplayablePackage>> = DataKey.create<List<DisplayablePackage>>("SELECTED_PACKAGES_DATA_CONTEXT")
internal val AnActionEvent.selectedPackage: DisplayablePackage?
get() = getData(SELECTED_PACKAGE_DATA_CONTEXT)
@@ -42,7 +43,7 @@ object PyPackagesUiComponents {
override fun onChosen(selectedValue: String?, finalChoice: Boolean): PopupStep<*>? {
return doFinalStep {
val repository = checkNotNull(selectedPackage.repository)
val specification = repository.findPackageSpecification(selectedPackage.name, selectedValue)
val specification = repository.findPackageSpecification(NormalizedPythonPackageName.from(selectedPackage.name).name, selectedValue)
PyPackageCoroutine.getIoScope(project).launch(Dispatchers.IO) {
project.service<PyPackagingToolWindowService>().installPackage(specification!!.toInstallRequest())
}
@@ -51,7 +52,7 @@ object PyPackagesUiComponents {
}, 8)
}
fun boxPanel(init: JPanel.() -> Unit) = object : JPanel() {
fun boxPanel(init: JPanel.() -> Unit): JPanel = object : JPanel() {
init {
layout = BoxLayout(this, BoxLayout.X_AXIS)
alignmentX = LEFT_ALIGNMENT
@@ -59,14 +60,14 @@ object PyPackagesUiComponents {
}
}
fun borderPanel(init: JPanel.() -> Unit) = object : JPanel() {
fun borderPanel(init: JPanel.() -> Unit): JPanel = object : JPanel() {
init {
layout = BorderLayout(0, 0)
init()
}
}
fun headerPanel(label: JLabel, component: JComponent?) = object : JPanel() {
fun headerPanel(label: JLabel, component: JComponent?): JPanel = object : JPanel() {
init {
background = UIUtil.getLabelBackground()
layout = BorderLayout()
@@ -81,6 +82,9 @@ object PyPackagesUiComponents {
override fun mouseClicked(e: MouseEvent?) {
component.isVisible = !component.isVisible
label.icon = if (component.isVisible) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
val parent = component.parent
parent.revalidate()
parent.repaint()
}
})
}
@@ -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.poetry.packaging
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.packageRequirements.PackageNode
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractor
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractor.Companion.parseTree
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractorProvider
import com.jetbrains.python.sdk.poetry.isPoetry
import com.jetbrains.python.sdk.poetry.runPoetryWithSdk
/**
* Extracts package requirements tree using Poetry package manager.
*/
internal class PoetryPackageRequirementsTreeExtractor(private val sdk: Sdk) : PythonPackageRequirementsTreeExtractor {
override suspend fun extract(pkg: PythonPackage, module: Module): PackageNode {
val data = runPoetryWithSdk(sdk, "show", "--tree", pkg.name).getOr {
thisLogger().info("extracting requirements for package ${pkg.name}: error. Output: \n${it.error}")
return PackageNode(NormalizedPythonPackageName.from(pkg.name))
}
thisLogger().info("extracting requirements for package ${pkg.name}: \n${data.lines()}")
return parseTree(data.lines())
}
}
private class PoetryPackageRequirementsTreeExtractorProvider : PythonPackageRequirementsTreeExtractorProvider {
override fun createExtractor(sdk: Sdk): PythonPackageRequirementsTreeExtractor? =
if (sdk.isPoetry) PoetryPackageRequirementsTreeExtractor(sdk) else null
}
@@ -6,12 +6,12 @@ import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.packageRequires.PythonPackageRequiresExtractor
import com.jetbrains.python.packaging.packageRequires.PythonPackageRequiresExtractorProvider
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementExtractor
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequiresExtractorProvider
import com.jetbrains.python.sdk.poetry.isPoetry
import com.jetbrains.python.sdk.poetry.runPoetryWithSdk
internal class PoetryPackageRequiresExtractor(private val sdk: Sdk) : PythonPackageRequiresExtractor {
internal class PoetryPackageRequirementExtractor(private val sdk: Sdk) : PythonPackageRequirementExtractor {
override suspend fun extract(pkg: PythonPackage, module: Module): List<NormalizedPythonPackageName> {
val data = runPoetryWithSdk(sdk, "show", pkg.name).getOr {
@@ -52,8 +52,8 @@ internal class PoetryPackageRequiresExtractor(private val sdk: Sdk) : PythonPack
}
private class PoetryRequiresExtractorProvider: PythonPackageRequiresExtractorProvider {
override fun createExtractor(sdk: Sdk): PythonPackageRequiresExtractor? {
override fun createExtractor(sdk: Sdk): PythonPackageRequirementExtractor? {
if (!sdk.isPoetry) return null
return PoetryPackageRequiresExtractor(sdk)
return PoetryPackageRequirementExtractor(sdk)
}
}
@@ -37,6 +37,7 @@ interface UvLowLevel {
suspend fun listOutdatedPackages(): PyResult<List<PythonOutdatedPackage>>
suspend fun listTopLevelPackages(): PyResult<List<PythonPackage>>
suspend fun listPackageRequirements(name: PythonPackage): PyResult<List<NormalizedPythonPackageName>>
suspend fun listPackageRequirementsTree(name: PythonPackage): PyResult<String>
suspend fun isProjectSynced(inexact: Boolean): PyExecResult<Boolean>
suspend fun isScriptSynced(inexact: Boolean, scriptPath: Path): PyExecResult<ScriptSyncCheckResult>
@@ -141,6 +141,13 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
return PyExecResult.success(parsePackageRequirements(out))
}
override suspend fun listPackageRequirementsTree(name: PythonPackage): PyResult<String> {
val out = uvCli.runUv(cwd, "tree", "--package", name.name)
.getOr { return it }
return PyExecResult.success(out)
}
override suspend fun installPackage(name: PythonPackageInstallRequest, options: List<String>): PyExecResult<Unit> {
uvCli.runUv(cwd, "pip", "install", *name.formatPackageName(), *options.toTypedArray())
.getOr { return it }
@@ -0,0 +1,41 @@
// 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.uv.packaging
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.packageRequirements.PackageNode
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractor
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractor.Companion.parseTree
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractorProvider
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.uv.UvSdkAdditionalData
import com.jetbrains.python.sdk.uv.impl.createUvCli
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import com.jetbrains.python.sdk.uv.isUv
import java.nio.file.Path
internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory: Path?) : PythonPackageRequirementsTreeExtractor {
override suspend fun extract(pkg: PythonPackage, module: Module): PackageNode {
val uvWorkingDirectory = uvWorkingDirectory ?: Path.of(module.basePath!!)
val uv = createUvLowLevel(uvWorkingDirectory, createUvCli())
val out = uv.listPackageRequirementsTree(pkg).getOr {
thisLogger().info("extracting requires for package ${pkg.name}: error. Output: \n${it.error}")
return PackageNode(NormalizedPythonPackageName.from(pkg.name), mutableListOf())
}
return parseTree(out.lines())
}
}
private class UvPackageRequirementsTreeExtractorProvider : PythonPackageRequirementsTreeExtractorProvider {
override fun createExtractor(sdk: Sdk): PythonPackageRequirementsTreeExtractor? {
if (!sdk.isUv) return null
val data = sdk.sdkAdditionalData as? UvSdkAdditionalData ?: return null
return UvPackageRequirementsTreeExtractor( data.uvWorkingDirectory)
}
}
@@ -6,15 +6,15 @@ import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.packageRequires.PythonPackageRequiresExtractor
import com.jetbrains.python.packaging.packageRequires.PythonPackageRequiresExtractorProvider
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementExtractor
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequiresExtractorProvider
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.uv.UvSdkAdditionalData
import com.jetbrains.python.sdk.uv.impl.createUvCli
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import java.nio.file.Path
internal class UvPackageRequiresExtractor(private val uvWorkingDirectory: Path?) : PythonPackageRequiresExtractor {
internal class UvPackageRequirementExtractor(private val uvWorkingDirectory: Path?) : PythonPackageRequirementExtractor {
override suspend fun extract(pkg: PythonPackage, module: Module): List<NormalizedPythonPackageName> {
val uvWorkingDirectory = uvWorkingDirectory ?: Path.of(module.basePath!!)
val uv = createUvLowLevel(uvWorkingDirectory, createUvCli())
@@ -26,8 +26,8 @@ internal class UvPackageRequiresExtractor(private val uvWorkingDirectory: Path?)
}
private class UvPackageRequiresExtractorProvider: PythonPackageRequiresExtractorProvider {
override fun createExtractor(sdk: Sdk): PythonPackageRequiresExtractor? {
override fun createExtractor(sdk: Sdk): PythonPackageRequirementExtractor? {
val data = sdk.sdkAdditionalData as? UvSdkAdditionalData ?: return null
return UvPackageRequiresExtractor(data.uvWorkingDirectory)
return UvPackageRequirementExtractor(data.uvWorkingDirectory)
}
}