@@ -10,7 +10,6 @@ import com.intellij.openapi.components.Service
1010import com.intellij.openapi.components.service
1111import com.intellij.openapi.fileEditor.FileEditorManagerEvent
1212import com.intellij.openapi.fileEditor.FileEditorManagerListener
13- import com.intellij.openapi.module.Module
1413import com.intellij.openapi.module.ModuleUtilCore
1514import com.intellij.openapi.options.ex.SingleConfigurableEditor
1615import com.intellij.openapi.project.Project
@@ -39,6 +38,7 @@ import com.jetbrains.python.packaging.toolwindow.model.*
3938import com.jetbrains.python.sdk.PythonSdkUtil
4039import com.jetbrains.python.sdk.pythonSdk
4140import kotlinx.coroutines.*
41+ import org.jetbrains.annotations.ApiStatus
4242import org.jetbrains.annotations.Nls
4343
4444@Service(Service .Level .PROJECT )
@@ -87,6 +87,68 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
8787 }
8888 }
8989
90+ private fun nameMatches (pkg : DisplayablePackage , query : String ): Boolean {
91+ val shouldUseStraightComparison = when (pkg) {
92+ is InstalledPackage -> isNonPipCondaPackage(pkg.instance)
93+ is RequirementPackage -> isNonPipCondaPackage(pkg.instance)
94+ else -> false
95+ }
96+
97+ return if (shouldUseStraightComparison) {
98+ StringUtil .containsIgnoreCase(pkg.name, query)
99+ } else {
100+ StringUtil .containsIgnoreCase(normalizePackageName(pkg.name), normalizePackageName(query))
101+ }
102+ }
103+
104+ private fun isNonPipCondaPackage (pkg : PythonPackage ): Boolean = pkg is CondaPackage && ! pkg.installedWithPip
105+
106+ private fun traversePackageTree (
107+ pkg : DisplayablePackage ,
108+ visited : MutableSet <String >,
109+ matches : MutableList <RequirementPackage >,
110+ query : String ,
111+ ) {
112+ if (! visited.add(pkg.name)) return
113+
114+ if (pkg is RequirementPackage && nameMatches(pkg, query)) {
115+ matches.add(pkg)
116+ }
117+
118+ for (requirementPackage in pkg.getRequirements()) {
119+ traversePackageTree(requirementPackage, visited, matches, query)
120+ }
121+ }
122+
123+ /* *
124+ * Finds all packages (both installed and requirements) that match the given query.
125+ */
126+ @ApiStatus.Internal
127+ fun findAllMatchingPackages (query : String ): List <DisplayablePackage > {
128+ val matchingInstalled = installedPackages.values.filter { nameMatches(it, query) }
129+ val matchingRequirements = mutableListOf<RequirementPackage >()
130+ val visited = mutableSetOf<String >()
131+
132+ for (pkg in installedPackages.values) {
133+ traversePackageTree(pkg, visited, matchingRequirements, query)
134+ }
135+
136+ return unifyPackages(matchingInstalled, matchingRequirements)
137+ }
138+
139+ /* *
140+ * Unifies packages with the same name according to the following rules:
141+ * 1. If both an installed package and a requirement package have the same name, keep only the installed package.
142+ * 2. If multiple requirement packages have the same name, keep only one of them.
143+ */
144+ private fun unifyPackages (installedPackages : List <InstalledPackage >, requirementPackages : List <RequirementPackage >): List <DisplayablePackage > {
145+ return (installedPackages + requirementPackages)
146+ .groupBy { it.name.lowercase() }
147+ .map { (_, packages) ->
148+ packages.find { it is InstalledPackage } ? : packages.first()
149+ }
150+ }
151+
90152 fun handleSearch (query : String ) {
91153 val manager = manager ? : return
92154 val prevSelected = toolWindowPanel?.getSelectedPackage()
@@ -95,21 +157,14 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
95157 if (query.isNotEmpty()) {
96158 searchJob?.cancel()
97159 searchJob = serviceScope.launch {
98- val installed = installedPackages.values.filter { pkg ->
99- when {
100- pkg.instance is CondaPackage && ! pkg.instance.installedWithPip -> StringUtil .containsIgnoreCase(pkg.name, query)
101- else -> StringUtil .containsIgnoreCase(normalizePackageName(pkg.name), normalizePackageName(query))
102- }
103- }
104-
105-
106- val packagesFromRepos = manager.repositoryManager.searchPackages(query).map {
107- sortPackagesForRepo(it.value, query, it.key)
108- }.toList()
160+ val allMatches = findAllMatchingPackages(query)
161+ val packagesFromRepos = manager.repositoryManager.searchPackages(query)
162+ .map { (repository, packages) -> sortPackagesForRepo(packages, query, repository) }
163+ .toList()
109164
110165 if (isActive) {
111166 withContext(Dispatchers .Main ) {
112- toolWindowPanel?.showSearchResult(installed , packagesFromRepos + invalidRepositories)
167+ toolWindowPanel?.showSearchResult(allMatches , packagesFromRepos + invalidRepositories)
113168 prevSelected?.name?.let { toolWindowPanel?.selectPackageName(it) }
114169 }
115170 }
@@ -147,7 +202,8 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
147202 handleActionCompleted(message(" python.packaging.notification.deleted" , selectedPackages.joinToString(" , " ) { it.name }))
148203 }
149204
150- internal suspend fun initForSdk (sdk : Sdk ? ) {
205+ @ApiStatus.Internal
206+ suspend fun initForSdk (sdk : Sdk ? ) {
151207 if (sdk == null ) {
152208 toolWindowPanel?.packageListController?.setLoadingState(false )
153209 }
@@ -234,7 +290,6 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
234290
235291 suspend fun refreshInstalledPackages () {
236292 val sdk = currentSdk ? : return
237- val targetModule = findTargetModule(sdk) ? : return
238293 val manager = manager ? : return
239294 withContext(Dispatchers .Default ) {
240295 val declaredPackages = manager.reloadDependencies()
@@ -245,9 +300,9 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
245300 processPackagesWithRequirementsTree(
246301 installedDeclaredPackages,
247302 treeExtractor,
248- targetModule
249303 )
250- } else {
304+ }
305+ else {
251306 emptyList()
252307 }
253308
@@ -262,9 +317,6 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
262317 }
263318 }
264319
265- private fun findTargetModule (sdk : Sdk ): Module ? =
266- project.modules.find { it.pythonSdk == sdk }
267-
268320 private suspend fun findInstalledDeclaredPackages (declaredPackages : List <PythonPackage >): List <PythonPackage > =
269321 manager?.listInstalledPackages()?.filter {
270322 it.name in declaredPackages.map { pkg -> pkg.name }
@@ -273,10 +325,9 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
273325 private suspend fun processPackagesWithRequirementsTree (
274326 packages : List <PythonPackage >,
275327 treeExtractor : PythonPackageRequirementsTreeExtractor ,
276- targetModule : Module ,
277328 ): List <InstalledPackage > {
278329 return packages.mapNotNull { pkg ->
279- val tree = treeExtractor.extract(pkg, targetModule )
330+ val tree = treeExtractor.extract(pkg)
280331 createInstalledPackageFromTree(pkg, tree)
281332 }
282333 }
0 commit comments