[python] support multi-module projects on Python Packages toolwindow (PY-54800)

(cherry picked from commit b525b804dadf9432314295ff9eed5571c97c02c7)

IJ-MR-102434

GitOrigin-RevId: ced58522f8bae1bc551df8737d120e2ce2545838
This commit is contained in:
Aleksei Kniazev
2023-02-06 17:09:22 +01:00
committed by intellij-monorepo-bot
parent 51255eec58
commit f52c2e22d0
4 changed files with 119 additions and 10 deletions

View File

@@ -23,6 +23,8 @@
<listener
class="com.jetbrains.python.inspections.PyInterpreterInspection$Visitor$CacheCleaner"
topic="com.intellij.openapi.projectRoots.ProjectJdkTable$Listener"/>
<listener class="com.jetbrains.python.packaging.toolwindow.PyPackagesToolWindowModuleAttachListener"
topic="com.intellij.platform.ModuleAttachListener"/>
</projectListeners>
<extensions defaultExtensionNs="com.intellij">

View File

@@ -0,0 +1,15 @@
// Copyright 2000-2023 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
import com.intellij.openapi.components.service
import com.intellij.openapi.module.Module
import com.intellij.platform.ModuleAttachListener
import java.nio.file.Path
class PyPackagesToolWindowModuleAttachListener : ModuleAttachListener {
override fun afterAttach(module: Module, primaryModule: Module?, imlFile: Path, tasks: MutableList<suspend () -> Unit>) {
tasks.add {
module.project.service<PyPackagingToolWindowService>().moduleAttached()
}
}
}

View File

@@ -10,7 +10,13 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.components.service
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.modules
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.popup.JBPopupFactory
@@ -38,6 +44,8 @@ import com.jetbrains.python.packaging.PyPackageUtil
import com.jetbrains.python.packaging.common.PythonLocalPackageSpecification
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonVcsPackageSpecification
import com.jetbrains.python.sdk.pythonSdk
import icons.PythonIcons
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
@@ -45,11 +53,13 @@ import kotlinx.coroutines.withContext
import java.awt.BorderLayout
import java.awt.Component
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.*
import javax.swing.event.DocumentEvent
import javax.swing.event.ListSelectionListener
class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolWindow) : SimpleToolWindowPanel(false, true), Disposable {
private val packagingScope = ApplicationManager.getApplication().coroutineScope.childScope(Dispatchers.Default)
@@ -80,7 +90,7 @@ class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolW
// layout
private var mainPanel: JPanel? = null
private var splitter: OnePixelSplitter? = null
private val leftPanel: JScrollPane
private var leftPanel: JComponent
private val rightPanel: JComponent
internal var contentVisible: Boolean
@@ -163,8 +173,8 @@ class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolW
}
tablesView = PyPackagingTablesView(project, packageListPanel, this)
leftPanel = ScrollPaneFactory.createScrollPane(packageListPanel, true)
leftPanel = createLeftPanel(service)
rightPanel = borderPanel {
add(borderPanel {
border = SideBorder(JBColor.GRAY, SideBorder.BOTTOM)
@@ -274,6 +284,61 @@ class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolW
setContent(mainPanel!!)
}
private fun createLeftPanel(service: PyPackagingToolWindowService): JComponent {
if (project.modules.size == 1) return ScrollPaneFactory.createScrollPane(packageListPanel, true)
val left = JPanel(BorderLayout()).apply {
border = BorderFactory.createEmptyBorder()
}
val modulePanel = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
border = SideBorder(NamedColorUtil.getBoundsColor(), SideBorder.RIGHT)
maximumSize = Dimension(80, maximumSize.height)
minimumSize = Dimension(50, minimumSize.height)
}
val moduleList = JBList(ModuleManager.getInstance(project).modules.toList().sortedBy { it.name }).apply {
selectionMode = ListSelectionModel.SINGLE_SELECTION
border = JBUI.Borders.empty()
val itemRenderer = JBLabel("", PythonIcons.Python.PythonClosed, JLabel.LEFT).apply {
border = JBUI.Borders.empty(0, 10)
}
cellRenderer = ListCellRenderer { _, value, _, _, _ -> itemRenderer.text = value.name; itemRenderer }
addListSelectionListener(ListSelectionListener { e ->
if (e.valueIsAdjusting) return@ListSelectionListener
val selectedModule = this@apply.selectedValue
val sdk = selectedModule.pythonSdk ?: return@ListSelectionListener
packagingScope.launch {
service.initForSdk(sdk)
}
})
}
val fileListener = object : FileEditorManagerListener {
override fun selectionChanged(event: FileEditorManagerEvent) {
if (project.modules.size > 1) {
val newFile = event.newFile ?: return
val module = ModuleUtilCore.findModuleForFile(newFile, project)
packagingScope.launch(Dispatchers.IO) {
val index = (moduleList.model as DefaultListModel<Module>).indexOf(module)
moduleList.selectionModel.setSelectionInterval(index, index)
}
}
}
}
service.project.messageBus
.connect(service)
.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, fileListener)
modulePanel.add(moduleList)
left.add(ScrollPaneFactory.createScrollPane(modulePanel, true), BorderLayout.WEST)
left.add(ScrollPaneFactory.createScrollPane(packageListPanel, true), BorderLayout.CENTER)
return left
}
private fun showInstallFromVcsDialog(service: PyPackagingToolWindowService): PythonVcsPackageSpecification? {
var editable = false
var link = ""
@@ -487,6 +552,15 @@ class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolW
packagingScope.cancel()
}
internal suspend fun recreateModulePanel() {
val newPanel = createLeftPanel(project.service<PyPackagingToolWindowService>())
withContext(Dispatchers.Main) {
leftPanel = newPanel
splitter?.firstComponent = leftPanel
splitter?.repaint()
}
}
companion object {
private const val HORIZONTAL_SPLITTER_KEY = "Python.PackagingToolWindow.Horizontal"
private const val VERTICAL_SPLITTER_KEY = "Python.PackagingToolWindow.Vertical"

View File

@@ -11,6 +11,9 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.options.ex.SingleConfigurableEditor
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressManager
@@ -27,8 +30,8 @@ import com.jetbrains.python.PythonHelper
import com.jetbrains.python.PythonHelpersLocator
import com.jetbrains.python.packaging.*
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.PythonPackageManagementListener
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.packagesByRepository
import com.jetbrains.python.packaging.repository.*
@@ -37,18 +40,18 @@ import com.jetbrains.python.run.applyHelperPackageToPythonPath
import com.jetbrains.python.run.buildTargetedCommandLine
import com.jetbrains.python.run.prepareHelperScriptExecution
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.sdk.sdkFlavor
import com.jetbrains.python.statistics.modules
import kotlinx.coroutines.*
import org.intellij.plugins.markdown.ui.preview.html.MarkdownUtil
import org.jetbrains.annotations.Nls
import kotlin.Comparator
@Service
class PyPackagingToolWindowService(val project: Project) : Disposable {
private lateinit var toolWindowPanel: PyPackagingToolWindowPanel
private var toolWindowPanel: PyPackagingToolWindowPanel? = null
lateinit var manager: PythonPackageManager
private var installedPackages: List<InstalledPackage> = emptyList()
internal var currentSdk: Sdk? = null
@@ -87,7 +90,7 @@ class PyPackagingToolWindowService(val project: Project) : Disposable {
if (isActive) {
withContext(Dispatchers.Main) {
toolWindowPanel.showSearchResult(installed, packagesFromRepos + invalidRepositories)
toolWindowPanel?.showSearchResult(installed, packagesFromRepos + invalidRepositories)
}
}
}
@@ -98,7 +101,7 @@ class PyPackagingToolWindowService(val project: Project) : Disposable {
PyPackagesViewData(repository, shownPackages, moreItems = packages.size - PACKAGES_LIMIT)
}.toList()
toolWindowPanel.resetSearch(installedPackages, packagesByRepository + invalidRepositories)
toolWindowPanel?.resetSearch(installedPackages, packagesByRepository + invalidRepositories)
}
}
@@ -112,7 +115,7 @@ class PyPackagingToolWindowService(val project: Project) : Disposable {
if (result.isSuccess) showPackagingNotification(message("python.packaging.notification.deleted", selectedPackage.name))
}
private suspend fun initForSdk(sdk: Sdk?) {
internal suspend fun initForSdk(sdk: Sdk?) {
val previousSdk = currentSdk
currentSdk = sdk
if (currentSdk != null) {
@@ -125,9 +128,9 @@ class PyPackagingToolWindowService(val project: Project) : Disposable {
}
}
withContext(Dispatchers.Main) {
toolWindowPanel.contentVisible = currentSdk != null
toolWindowPanel?.contentVisible = currentSdk != null
if (currentSdk == null || currentSdk != previousSdk) {
toolWindowPanel.setEmpty()
toolWindowPanel?.setEmpty()
}
}
}
@@ -151,6 +154,17 @@ class PyPackagingToolWindowService(val project: Project) : Disposable {
}
}
})
connection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener {
override fun selectionChanged(event: FileEditorManagerEvent) {
val newFile = event.newFile ?: return
val module = ModuleUtilCore.findModuleForFile(newFile, project)
val sdk = PythonSdkUtil.findPythonSdk(module) ?: return
serviceScope.launch(Dispatchers.IO) {
initForSdk(sdk)
}
}
})
}
@@ -317,6 +331,10 @@ class PyPackagingToolWindowService(val project: Project) : Disposable {
.toList()
}
internal suspend fun moduleAttached() {
toolWindowPanel?.recreateModulePanel()
}
companion object {
private const val PACKAGES_LIMIT = 50
private fun createNameComparator(query: String, url: String): Comparator<String> {