@@ -10,7 +10,6 @@ import com.intellij.openapi.components.Service
10
10
import com.intellij.openapi.components.service
11
11
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
12
12
import com.intellij.openapi.fileEditor.FileEditorManagerListener
13
- import com.intellij.openapi.module.Module
14
13
import com.intellij.openapi.module.ModuleUtilCore
15
14
import com.intellij.openapi.options.ex.SingleConfigurableEditor
16
15
import com.intellij.openapi.project.Project
@@ -39,6 +38,7 @@ import com.jetbrains.python.packaging.toolwindow.model.*
39
38
import com.jetbrains.python.sdk.PythonSdkUtil
40
39
import com.jetbrains.python.sdk.pythonSdk
41
40
import kotlinx.coroutines.*
41
+ import org.jetbrains.annotations.ApiStatus
42
42
import org.jetbrains.annotations.Nls
43
43
44
44
@Service(Service .Level .PROJECT )
@@ -87,6 +87,68 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
87
87
}
88
88
}
89
89
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
+
90
152
fun handleSearch (query : String ) {
91
153
val manager = manager ? : return
92
154
val prevSelected = toolWindowPanel?.getSelectedPackage()
@@ -95,21 +157,14 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
95
157
if (query.isNotEmpty()) {
96
158
searchJob?.cancel()
97
159
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()
109
164
110
165
if (isActive) {
111
166
withContext(Dispatchers .Main ) {
112
- toolWindowPanel?.showSearchResult(installed , packagesFromRepos + invalidRepositories)
167
+ toolWindowPanel?.showSearchResult(allMatches , packagesFromRepos + invalidRepositories)
113
168
prevSelected?.name?.let { toolWindowPanel?.selectPackageName(it) }
114
169
}
115
170
}
@@ -147,7 +202,8 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
147
202
handleActionCompleted(message(" python.packaging.notification.deleted" , selectedPackages.joinToString(" , " ) { it.name }))
148
203
}
149
204
150
- internal suspend fun initForSdk (sdk : Sdk ? ) {
205
+ @ApiStatus.Internal
206
+ suspend fun initForSdk (sdk : Sdk ? ) {
151
207
if (sdk == null ) {
152
208
toolWindowPanel?.packageListController?.setLoadingState(false )
153
209
}
@@ -234,7 +290,6 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
234
290
235
291
suspend fun refreshInstalledPackages () {
236
292
val sdk = currentSdk ? : return
237
- val targetModule = findTargetModule(sdk) ? : return
238
293
val manager = manager ? : return
239
294
withContext(Dispatchers .Default ) {
240
295
val declaredPackages = manager.reloadDependencies()
@@ -245,9 +300,9 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
245
300
processPackagesWithRequirementsTree(
246
301
installedDeclaredPackages,
247
302
treeExtractor,
248
- targetModule
249
303
)
250
- } else {
304
+ }
305
+ else {
251
306
emptyList()
252
307
}
253
308
@@ -262,9 +317,6 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
262
317
}
263
318
}
264
319
265
- private fun findTargetModule (sdk : Sdk ): Module ? =
266
- project.modules.find { it.pythonSdk == sdk }
267
-
268
320
private suspend fun findInstalledDeclaredPackages (declaredPackages : List <PythonPackage >): List <PythonPackage > =
269
321
manager?.listInstalledPackages()?.filter {
270
322
it.name in declaredPackages.map { pkg -> pkg.name }
@@ -273,10 +325,9 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
273
325
private suspend fun processPackagesWithRequirementsTree (
274
326
packages : List <PythonPackage >,
275
327
treeExtractor : PythonPackageRequirementsTreeExtractor ,
276
- targetModule : Module ,
277
328
): List <InstalledPackage > {
278
329
return packages.mapNotNull { pkg ->
279
- val tree = treeExtractor.extract(pkg, targetModule )
330
+ val tree = treeExtractor.extract(pkg)
280
331
createInstalledPackageFromTree(pkg, tree)
281
332
}
282
333
}
0 commit comments