mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
f5dbbb76e7
commit
4cc34127cf
@@ -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)"
|
||||
}
|
||||
|
||||
+5
-5
@@ -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")
|
||||
+84
@@ -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
|
||||
}
|
||||
+145
-34
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-4
@@ -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)
|
||||
|
||||
|
||||
+9
-5
@@ -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()
|
||||
|
||||
-69
@@ -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()
|
||||
}
|
||||
}
|
||||
+146
@@ -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()
|
||||
}
|
||||
}
|
||||
-122
@@ -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)
|
||||
}
|
||||
}
|
||||
-57
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-25
@@ -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()
|
||||
}
|
||||
}
|
||||
-162
@@ -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"
|
||||
}
|
||||
}
|
||||
+85
@@ -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
|
||||
}
|
||||
}
|
||||
+195
@@ -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()
|
||||
}
|
||||
+97
@@ -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
|
||||
}
|
||||
}
|
||||
+27
@@ -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
|
||||
}
|
||||
}
|
||||
+81
@@ -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)
|
||||
}
|
||||
+108
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+34
@@ -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
|
||||
}
|
||||
+5
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user