diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 11fd1d99..60f63924 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -2,7 +2,7 @@ name: Publish snapshot to TBE on: push: - branches: [ master ] + branches: [ releases/232 ] jobs: publish: @@ -26,3 +26,5 @@ jobs: GRADLE_ENTERPRISE_KEY: ${{ secrets.GRADLE_ENTERPRISE_KEY }} MAVEN_SPACE_PASSWORD: ${{ secrets.MAVEN_SPACE_PASSWORD }} MAVEN_SPACE_USERNAME: ${{ secrets.MAVEN_SPACE_USERNAME }} + RUN_NUMBER: ${{ github.run_number }} + RUN_ATTEMPT: ${{ github.run_attempt }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..215272e3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +## Code of Conduct + +This project and the corresponding community is governed by +the [JetBrains Open Source and Community Code of Conduct](https://confluence.jetbrains.com/display/ALL/JetBrains+Open+Source+and+Community+Code+of+Conduct). +Please make sure you read it. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8541f77f --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2000-2021 JetBrains s.r.o. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..038a5bfe --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# Package Search [![official JetBrains project](https://jb.gg/badges/official-flat-square.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) + +Package Search is an IntelliJ plugin that allows you to search for packages from the editor. It supports searching for +packages from the following package managers by default: + +- [Maven](https://maven.apache.org/) +- [Gradle](https://gradle.org/) +- [Amper](https://blog.jetbrains.com/blog/2023/11/09/amper-improving-the-build-tooling-user-experience/) + +It also supports Kotlin Multiplatform projects for both for Gradle and Amper. + +![Package Search](https://plugins.jetbrains.com/files/12507/screenshot_2db7914e-4a6a-45a1-aa34-ed00b150cf62) +![Package Search](https://plugins.jetbrains.com/files/12507/screenshot_26124d52-4baf-4e5c-bff3-1ecb81efd83c) + +# Installation + +You can download the plugin from the [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/12507-package-search) +or directly in IntelliJ by going to `Preferences > Plugins > Marketplace` and searching for `Package Search`. + +The plugin is compatible with IntelliJ 2023.2 and newer. + +# Building + +To build the plugin, run the following command: + +```shell +./gradlew :plugin:buildShadowPlugin +``` + +To run the plugin, run the following command: + +```shell +./gradlew :plugin:runIde +``` diff --git a/build.gradle.kts b/build.gradle.kts index 7d99dc55..32e5c558 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,6 @@ @file:Suppress("UnstableApiUsage") import java.lang.System.getenv -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime import org.jetbrains.packagesearch.gradle.pkgsSpace plugins { @@ -14,7 +11,7 @@ plugins { allprojects { group = "org.jetbrains.packagesearch" - val baseVersion = "2.0.0-SNAPSHOT" + val baseVersion = "232.10227-SNAPSHOT" version = when (val ref = getenv("GITHUB_REF")) { null -> baseVersion else -> when { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 27a63437..17199620 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -30,5 +30,4 @@ dependencies { implementation(packageSearchCatalog.kotlinx.serialization.json) implementation("com.squareup:kotlinpoet:1.14.2") implementation("io.github.pdvrieze.xmlutil:serialization:0.86.2") - implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1") } diff --git a/buildSrc/src/main/kotlin/org/jetbrains/packagesearch/gradle/ConfigureGradleIntellijPlugin.kt b/buildSrc/src/main/kotlin/org/jetbrains/packagesearch/gradle/ConfigureGradleIntellijPlugin.kt index 81d2a96c..fdc682f1 100644 --- a/buildSrc/src/main/kotlin/org/jetbrains/packagesearch/gradle/ConfigureGradleIntellijPlugin.kt +++ b/buildSrc/src/main/kotlin/org/jetbrains/packagesearch/gradle/ConfigureGradleIntellijPlugin.kt @@ -15,7 +15,7 @@ fun Project.configureGradleIntellijPlugin(packageSearchExtension: PackageSearchE plugins.withId("org.jetbrains.intellij") { extensions.withType { - version = "233-EAP-SNAPSHOT" + version = "232.10227.8" instrumentCode = false downloadSources = !isCI } @@ -24,9 +24,6 @@ fun Project.configureGradleIntellijPlugin(packageSearchExtension: PackageSearchE relocate("io.ktor", "shadow.io.ktor") relocate("kotlinx.serialization", "shadow.kotlinx.serialization") relocate("kotlinx.datetime", "shadow.kotlinx.datetime") - relocate("androidx", "shadow.androidx") - relocate("org.jetbrains.jewel", "shadow.org.jetbrains.jewel") - relocate("org.jetbrains.compose", "shadow.org.jetbrains.compose") exclude { it.name.containsAny(packageSearchExtension.librariesToDelete.get()) && !it.name.containsAny(packageSearchExtension.librariesToKeep.get()) @@ -43,4 +40,4 @@ fun Project.configureGradleIntellijPlugin(packageSearchExtension: PackageSearchE } } } -} \ No newline at end of file +} diff --git a/package-search-api-models b/package-search-api-models index 908fd5ce..dabcc4de 160000 --- a/package-search-api-models +++ b/package-search-api-models @@ -1 +1 @@ -Subproject commit 908fd5ce55cdea67c28e3e24074aa99a33d339a7 +Subproject commit dabcc4dee952a94a35df58686ec6b31be24f9c65 diff --git a/packagesearch.versions.toml b/packagesearch.versions.toml index 50d21b44..64c1e6cd 100644 --- a/packagesearch.versions.toml +++ b/packagesearch.versions.toml @@ -9,7 +9,7 @@ dokka = "1.9.10" foojay = "0.5.0" gradlePublishPlugin = "1.1.0" idea = "2023.1.2" -ideaGradlePlugin = "1.14.1" +ideaGradlePlugin = "1.16.1" junit = "5.10.0" junit4 = "4.13.2" kotlin = "1.9.20" @@ -37,8 +37,10 @@ ij-platform-ide-core = { module = "com.jetbrains.intellij.platform:ide-core", ve ij-platform-ide-impl = { module = "com.jetbrains.intellij.platform:ide-impl", version.ref = "idea" } jewel-ui = { module = "org.jetbrains.jewel:jewel-ui", version.ref = "jewel" } jewel-foundation = { module = "org.jetbrains.jewel:jewel-foundation", version.ref = "jewel" } +jewel-standalone = { module= "org.jetbrains.jewel:jewel-int-ui-standalone", version.ref = "jewel" } jewel-bridge-ij232 = { module = "org.jetbrains.jewel:jewel-ide-laf-bridge", version.ref = "jewel-bridge-232" } jewel-bridge-ij233 = { module = "org.jetbrains.jewel:jewel-ide-laf-bridge", version.ref = "jewel-bridge-233" } +compose-desktop-components-splitpane = { module = "org.jetbrains.compose.components:components-splitpane", version.ref = "composeDesktop" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index afabc733..b9b1f0d6 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -1,8 +1,6 @@ @file:Suppress("UnstableApiUsage") -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime +import kotlin.math.max import org.jetbrains.intellij.tasks.PublishPluginTask import org.jetbrains.packagesearch.gradle.lafFile import org.jetbrains.packagesearch.gradle.logCategoriesFile @@ -42,6 +40,10 @@ packagesearch { isRunIdeEnabled = true } +intellij { + plugins.add("org.jetbrains.idea.reposearch") +} + val tooling: Configuration by configurations.creating { isCanBeResolved = true } @@ -52,8 +54,13 @@ dependencies { implementation(compose.desktop.macos_arm64) implementation(compose.desktop.macos_x64) implementation(compose.desktop.windows_x64) - implementation(packageSearchCatalog.kotlinx.serialization.core) implementation(packageSearchCatalog.jewel.bridge.ij233) + implementation(packageSearchCatalog.kotlinx.serialization.core) + implementation(packageSearchCatalog.compose.desktop.components.splitpane){ + exclude(group = "org.jetbrains.compose.runtime") + exclude(group = "org.jetbrains.compose.foundation") + } + implementation(packageSearchCatalog.jewel.bridge.ij232) implementation(packageSearchCatalog.ktor.client.logging) implementation(packageSearchCatalog.packagesearch.api.models) implementation(projects.plugin.gradle.base) @@ -95,38 +102,27 @@ tasks { prepareSandbox { runtimeClasspathFiles = tooling } - val snapshotDateSuffix = buildString { - val now = Clock.System.now().toLocalDateTime(TimeZone.UTC) - append(now.year) - append(now.monthNumber) - append(now.dayOfMonth) - append(now.hour.toString().padStart(2, '0')) - append(now.minute.toString().padStart(2, '0')) - append(now.second.toString().padStart(2, '0')) - } + + val runNumber = System.getenv("RUN_NUMBER")?.toInt() ?: 0 + val runAttempt = System.getenv("RUN_ATTEMPT")?.toInt() ?: 0 + val snapshotMinorVersion = max(0, runNumber + runAttempt - 1) + val versionString = project.version.toString() + patchPluginXml { pluginId = pkgsPluginId - version = when { - project.version.toString().endsWith("-SNAPSHOT") -> "${project.version}-$snapshotDateSuffix" - else -> project.version.toString() - } + version = versionString.replace("-SNAPSHOT", ".$snapshotMinorVersion") } val buildShadowPlugin by registering(Zip::class) { group = "intellij" from(shadowJar) { - rename { - "package-search-plugin" + when { - it.endsWith("-SNAPSHOT.jar") -> it.replace(".jar", "-$snapshotDateSuffix.jar") - .also { logger.lifecycle("Snapshot version -> $it") } - else -> it - } - } + rename { "packageSearch.jar" } } from(tooling) { rename { "gradle-tooling.jar" } } into("$pkgsPluginId/lib") - archiveFileName.set("packagesearch-plugin.zip") + archiveFileName = "packageSearch-${project.version}.zip" + .replace("-SNAPSHOT", ".$snapshotMinorVersion") destinationDirectory = layout.buildDirectory.dir("distributions") } diff --git a/plugin/core/build.gradle.kts b/plugin/core/build.gradle.kts index 981fb2a0..d1e0014d 100644 --- a/plugin/core/build.gradle.kts +++ b/plugin/core/build.gradle.kts @@ -1,5 +1,6 @@ @file:Suppress("UnstableApiUsage") +import kotlin.math.max import org.jetbrains.packagesearch.gradle.GeneratePackageSearchObject @@ -42,6 +43,11 @@ kotlin.sourceSets.main { val pkgsPluginId: String by project +val runNumber = System.getenv("RUN_NUMBER")?.toInt() ?: 0 +val runAttempt = System.getenv("RUN_ATTEMPT")?.toInt() ?: 0 +val snapshotMinorVersion = max(0, runNumber + runAttempt - 1) +val versionString = project.version.toString() + tasks { withType { environment("DB_PATH", layout.buildDirectory.file("tests/cache.db").get().asFile.absolutePath) @@ -49,6 +55,7 @@ tasks { val generatePluginDataSources by registering(GeneratePackageSearchObject::class) { pluginId = pkgsPluginId outputDir = generatedDir + pluginVersion = versionString.replace("-SNAPSHOT", ".$snapshotMinorVersion") packageName = "com.jetbrains.packagesearch.plugin.core" } sourcesJar { diff --git a/plugin/gradle/base/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/GradleModuleProvider.kt b/plugin/gradle/base/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/GradleModuleProvider.kt index 0a5629c0..eebf5f17 100644 --- a/plugin/gradle/base/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/GradleModuleProvider.kt +++ b/plugin/gradle/base/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/GradleModuleProvider.kt @@ -9,9 +9,11 @@ import com.jetbrains.packagesearch.plugin.gradle.utils.getDeclaredDependencies import com.jetbrains.packagesearch.plugin.gradle.utils.getDeclaredKnownRepositories import kotlinx.coroutines.flow.FlowCollector import org.jetbrains.packagesearch.api.v3.ApiMavenRepository +import org.jetbrains.packagesearch.api.v3.search.androidPackages import org.jetbrains.packagesearch.api.v3.search.buildPackageTypes import org.jetbrains.packagesearch.api.v3.search.javaApi import org.jetbrains.packagesearch.api.v3.search.javaRuntime +import org.jetbrains.packagesearch.api.v3.search.jvmGradlePackages import org.jetbrains.packagesearch.api.v3.search.libraryElements class GradleModuleProvider : AbstractGradleModuleProvider() { @@ -49,36 +51,10 @@ class GradleModuleProvider : AbstractGradleModuleProvider() { compatiblePackageTypes = buildPackageTypes { mavenPackages() when { - model.isKotlinJvmApplied -> gradlePackages { - mustBeRootPublication = true - variant { - javaApi() - javaRuntime() - libraryElements("jar") - } - } - - model.isKotlinAndroidApplied -> { - gradlePackages { - mustBeRootPublication = true - variant { - javaApi() - javaRuntime() - libraryElements("aar") - } - } - gradlePackages { - mustBeRootPublication = true - variant { - javaApi() - javaRuntime() - libraryElements("jar") - } - } - } - + model.isKotlinAndroidApplied -> androidPackages() + model.isJavaApplied -> jvmGradlePackages("jar") else -> gradlePackages { - mustBeRootPublication = true + isRootPublication = true } } }, diff --git a/plugin/gradle/kmp/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/KotlinMultiplatformModuleProvider.kt b/plugin/gradle/kmp/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/KotlinMultiplatformModuleProvider.kt index 23f51ff2..2e6a454d 100644 --- a/plugin/gradle/kmp/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/KotlinMultiplatformModuleProvider.kt +++ b/plugin/gradle/kmp/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/KotlinMultiplatformModuleProvider.kt @@ -34,7 +34,7 @@ class KotlinMultiplatformModuleProvider : AbstractGradleModuleProvider() { module: Module, model: PackageSearchGradleModel, ) { - if (model.isKotlinMultiplatformApplied) + if (model.isKotlinMultiplatformApplied && !model.isAmperApplied) MppCompilationInfoProvider.sourceSetsMap(project, model.projectDir) .collect { compilationModel -> val variants = module.getKMPVariants( @@ -77,7 +77,7 @@ class KotlinMultiplatformModuleProvider : AbstractGradleModuleProvider() { compatiblePackageTypes = buildPackageTypes { mavenPackages() gradlePackages { - mustBeRootPublication = true + isRootPublication = true } }, availableScopes = availableScopes, @@ -95,7 +95,7 @@ class KotlinMultiplatformModuleProvider : AbstractGradleModuleProvider() { val rawDeclaredSourceSetDependencies = MppDependencyModifier .dependenciesBySourceSet(this@getKMPVariants) ?.filterNotNullValues() - ?.mapValues { readAction { it.value.artifacts().map { it.toGradleDependencyModel() } } } + ?.mapValues { readAction { it.value.artifacts().map { it.toGradleDependencyModel() } }.distinct() } ?: emptyMap() val packageIds = rawDeclaredSourceSetDependencies @@ -105,7 +105,7 @@ class KotlinMultiplatformModuleProvider : AbstractGradleModuleProvider() { .distinct() .map { it.packageId } - val dependencyInfo = getPackageInfoByIdHashes(packageIds.map { ApiPackage.hashPackageId(it) }.toSet()) + val dependencyInfo = getPackageInfoByIdHashes(packageIds.map { ApiPackage.hashPackageId(it) }.toSet()) val declaredSourceSetDependencies = rawDeclaredSourceSetDependencies @@ -133,19 +133,16 @@ class KotlinMultiplatformModuleProvider : AbstractGradleModuleProvider() { declaredDependencies = declaredSourceSetDependencies[sourceSetName] ?: emptyList(), attributes = compilationTargets.buildAttributes(), compatiblePackageTypes = buildPackageTypes { - if (compilationTargets.singleOrNull() == Jvm) { - mavenPackages() - } else gradlePackages { + gradlePackages { kotlinMultiplatform { compilationTargets.forEach { compilationTarget -> - when (compilationTarget) { - is Js -> when (compilationTarget.compiler) { + when { + compilationTarget is Js -> when (compilationTarget.compiler) { Js.Compiler.IR -> jsIr() Js.Compiler.LEGACY -> jsLegacy() } - - is Native -> native(compilationTarget.target) - else -> {} + compilationTarget is Native -> native(compilationTarget.target) + compilationTarget == MppCompilationInfoModel.Wasm -> wasm() } } when { @@ -161,5 +158,6 @@ class KotlinMultiplatformModuleProvider : AbstractGradleModuleProvider() { sourceSetVariants + dependenciesBlockVariant.await() } + } diff --git a/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/GradleDependencyModel.kt b/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/GradleDependencyModel.kt index c1a9b117..e9ab513e 100644 --- a/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/GradleDependencyModel.kt +++ b/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/GradleDependencyModel.kt @@ -12,4 +12,28 @@ data class GradleDependencyModel( val packageId get() = "maven:$groupId:$artifactId" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GradleDependencyModel + + if (groupId != other.groupId) return false + if (artifactId != other.artifactId) return false + if (version != other.version) return false + if (configuration != other.configuration) return false + + return true + } + + override fun hashCode(): Int { + var result = groupId.hashCode() + result = 31 * result + artifactId.hashCode() + result = 31 * result + (version?.hashCode() ?: 0) + result = 31 * result + configuration.hashCode() + return result + } + + } \ No newline at end of file diff --git a/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/PackageSearchGradleModel.kt b/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/PackageSearchGradleModel.kt index 5d6b7393..d740b34d 100644 --- a/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/PackageSearchGradleModel.kt +++ b/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/PackageSearchGradleModel.kt @@ -9,14 +9,15 @@ data class PackageSearchGradleModel( @Serializable(with = NioPathSerializer::class) val projectDir: Path, val configurations: List, val repositories: List, - val isKotlinJvmApplied: Boolean, + val isJavaApplied: Boolean, + val isAmperApplied: Boolean, val isKotlinAndroidApplied: Boolean, val isKotlinMultiplatformApplied: Boolean, val projectIdentityPath: String, val projectName: String, val rootProjectName: String, @Serializable(with = NioPathSerializer::class) val buildFilePath: Path?, - @Serializable(with = NioPathSerializer::class) val rootProjectPath: Path + @Serializable(with = NioPathSerializer::class) val rootProjectPath: Path, ) { @Serializable diff --git a/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/PackageSearchProjectResolverExtension.kt b/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/PackageSearchProjectResolverExtension.kt index 3cd2870d..89748e62 100644 --- a/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/PackageSearchProjectResolverExtension.kt +++ b/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/PackageSearchProjectResolverExtension.kt @@ -43,7 +43,8 @@ internal fun PackageSearchGradleJavaModel.toPackageSearchModel() = ) }, repositories = repositoryUrls, - isKotlinJvmApplied = isKotlinJvmApplied, + isJavaApplied = isJavaApplied, + isAmperApplied = isAmperApplied, isKotlinAndroidApplied = isKotlinAndroidApplied, isKotlinMultiplatformApplied = isKotlinMultiplatformApplied, rootProjectName = rootProjectName, diff --git a/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/utils/Utils.kt b/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/utils/Utils.kt index 89005384..494db97a 100644 --- a/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/utils/Utils.kt +++ b/plugin/gradle/src/main/kotlin/com/jetbrains/packagesearch/plugin/gradle/utils/Utils.kt @@ -34,7 +34,8 @@ val Module.gradleIdentityPathOrNull: String? ?.data ?.gradleIdentityPathOrNull -fun PackageSearchModuleBuilderContext.getGradleModelRepository(): CoroutineObjectRepository = +context(PackageSearchModuleBuilderContext) +fun getGradleModelRepository(): CoroutineObjectRepository = projectCaches.getRepository("gradle") val Project.gradleSyncNotifierFlow: Flow diff --git a/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleJavaModel.java b/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleJavaModel.java index cc08a2b6..edf0daa8 100644 --- a/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleJavaModel.java +++ b/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleJavaModel.java @@ -11,7 +11,9 @@ public interface PackageSearchGradleJavaModel extends Serializable { List getConfigurations(); List getRepositoryUrls(); String getRootProjectName(); - boolean isKotlinJvmApplied(); + boolean isJavaApplied(); + + boolean isAmperApplied(); boolean isKotlinAndroidApplied(); boolean isKotlinMultiplatformApplied(); String getBuildFilePath(); diff --git a/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleJavaModelImpl.java b/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleJavaModelImpl.java index 7b567ac9..5e97d027 100644 --- a/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleJavaModelImpl.java +++ b/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleJavaModelImpl.java @@ -9,7 +9,8 @@ public class PackageSearchGradleJavaModelImpl implements PackageSearchGradleJava private final List repositories; private final String projectIdentityPath; - boolean isKotlinJvmApplied; + boolean isJavaApplied; + private boolean isAmperApplied; boolean isKotlinMultiplatformApplied; boolean isKotlinAndroidApplied; private final String projectName; @@ -24,7 +25,8 @@ public PackageSearchGradleJavaModelImpl( String projectIdentityPath, List configurations, List repositories, - boolean isKotlinJvmApplied, + boolean isJavaApplied, + boolean isAmperApplied, boolean isKotlinMultiplatformApplied, boolean isKotlinAndroidApplied, String buildFilePath, @@ -34,13 +36,14 @@ public PackageSearchGradleJavaModelImpl( this.configurations = configurations; this.repositories = repositories; this.projectIdentityPath = projectIdentityPath; - this.isKotlinJvmApplied = isKotlinJvmApplied; + this.isJavaApplied = isJavaApplied; this.isKotlinMultiplatformApplied = isKotlinMultiplatformApplied; this.isKotlinAndroidApplied = isKotlinAndroidApplied; this.projectName = projectName; this.rootProjectName = rootProjectName; this.buildFilePath = buildFilePath; this.rootProjectPath = rootProjectPath; + this.isAmperApplied = isAmperApplied; } @Override @@ -73,8 +76,13 @@ public boolean isKotlinAndroidApplied() { return isKotlinAndroidApplied; } - public boolean isKotlinJvmApplied() { - return isKotlinJvmApplied; + public boolean isJavaApplied() { + return isJavaApplied; + } + + @Override + public boolean isAmperApplied() { + return isAmperApplied; } @Override diff --git a/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleModelBuilder.java b/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleModelBuilder.java index b6ba7fb2..798bddad 100644 --- a/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleModelBuilder.java +++ b/plugin/gradle/tooling/src/main/java/com/jetbrains/packagesearch/plugin/gradle/tooling/PackageSearchGradleModelBuilder.java @@ -10,7 +10,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.gradle.tooling.AbstractModelBuilderService; import org.jetbrains.plugins.gradle.tooling.ErrorMessageBuilder; -import org.jetbrains.plugins.gradle.tooling.Message; import org.jetbrains.plugins.gradle.tooling.ModelBuilderContext; import java.util.ArrayList; @@ -87,7 +86,8 @@ public PackageSearchGradleJavaModel buildAll( projectIdentityPath, configurations, repositories, - project.getPluginManager().hasPlugin("org.jetbrains.kotlin.jvm"), + project.getPluginManager().hasPlugin("org.gradle.java"), + project.getPluginManager().hasPlugin("org.jetbrains.amper.settings.plugin"), project.getPluginManager().hasPlugin("org.jetbrains.kotlin.multiplatform"), project.getPluginManager().hasPlugin("org.jetbrains.kotlin.android"), buildFilePath, @@ -100,20 +100,14 @@ public boolean canBuild(String modelName) { return modelName.equals(PackageSearchGradleJavaModel.class.getName()); } + @NotNull @Override - public void reportErrorMessage( - @NotNull String modelName, - @NotNull Project project, - @NotNull ModelBuilderContext context, - @NotNull Exception exception - ) { - context.getMessageReporter() - .createMessage() - .withException(exception) - .withKind(Message.Kind.ERROR) - .withGroup("gradle.packageSearch") - .withText("Error while building Package Search Gradle model") - .reportMessage(project); + public ErrorMessageBuilder getErrorMessageBuilder(@NotNull Project project, @NotNull Exception e) { + return ErrorMessageBuilder + .create(project, e, "Gradle import errors") + .withDescription("Unable to import resolved versions " + + "from configurations in project ''${project.name}'' for" + + " the Dependencies toolwindow."); } } diff --git a/plugin/maven/src/main/kotlin/com/jetbrains/packagesearch/plugin/maven/MavenUtils.kt b/plugin/maven/src/main/kotlin/com/jetbrains/packagesearch/plugin/maven/MavenUtils.kt index 18459430..b384abdd 100644 --- a/plugin/maven/src/main/kotlin/com/jetbrains/packagesearch/plugin/maven/MavenUtils.kt +++ b/plugin/maven/src/main/kotlin/com/jetbrains/packagesearch/plugin/maven/MavenUtils.kt @@ -42,6 +42,7 @@ import org.jetbrains.packagesearch.api.v3.ApiRepository import org.jetbrains.packagesearch.api.v3.search.buildPackageTypes import org.jetbrains.packagesearch.api.v3.search.javaApi import org.jetbrains.packagesearch.api.v3.search.javaRuntime +import org.jetbrains.packagesearch.api.v3.search.jvmMavenPackages import org.jetbrains.packagesearch.maven.POM_XML_NAMESPACE import org.jetbrains.packagesearch.maven.ProjectObjectModel import org.jetbrains.packagesearch.maven.decodeFromString @@ -110,21 +111,7 @@ suspend fun Module.toPackageSearch( declaredDependencies = declaredDependencies, availableScopes = commonScopes.plus(declaredDependencies.mapNotNull { it.declaredScope }).distinct(), compatiblePackageTypes = buildPackageTypes { - mavenPackages() - gradlePackages { - mustBeRootPublication = false - variant { - mustHaveFilesAttribute = true - javaApi() - } - } - gradlePackages { - mustBeRootPublication = false - variant { - mustHaveFilesAttribute = true - javaRuntime() - } - } + jvmMavenPackages() }, nativeModule = this ) diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/PackageSearchToolWindowFactory.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/PackageSearchToolWindowFactory.kt index 4013e26a..18343f4f 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/PackageSearchToolWindowFactory.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/PackageSearchToolWindowFactory.kt @@ -1,31 +1,21 @@ package com.jetbrains.packagesearch.plugin -import androidx.compose.runtime.CompositionLocalProvider import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory -import com.jetbrains.packagesearch.plugin.ui.LocalComponentManager +import com.jetbrains.packagesearch.plugin.ui.PackageSearchTheme import com.jetbrains.packagesearch.plugin.ui.PackageSearchToolwindow -import com.jetbrains.packagesearch.plugin.ui.panels.packages.packageSearchGlobalColors -import com.jetbrains.packagesearch.plugin.ui.panels.packages.packageSearchTabStyle import com.jetbrains.packagesearch.plugin.utils.installActions import org.jetbrains.jewel.bridge.addComposeTab -import org.jetbrains.jewel.foundation.LocalGlobalColors -import org.jetbrains.jewel.ui.component.styling.LocalDefaultTabStyle class PackageSearchToolWindowFactory : ToolWindowFactory, DumbAware { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { toolWindow.installActions(project) toolWindow.addComposeTab(PackageSearchBundle.message("packagesearch.title.tab")) { - CompositionLocalProvider( - LocalComponentManager provides project, - LocalGlobalColors provides packageSearchGlobalColors(), - LocalDefaultTabStyle provides packageSearchTabStyle() - ) { + PackageSearchTheme(project) { PackageSearchToolwindow() } } } } - diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/fus/FUSGroupIds.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/fus/FUSGroupIds.kt new file mode 100644 index 00000000..c77eed8d --- /dev/null +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/fus/FUSGroupIds.kt @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright 2000-2022 JetBrains s.r.o. and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +package com.jetbrains.packagesearch.plugin.fus + +object FUSGroupIds { + + const val GROUP_ID = "packagesearch" + + // FIELDS + const val IS_SEARCH_HEADER = "is_search_header" + const val PANEL_OPENED = "info_panel_opened" + const val GO_TO_SOURCE = "got_to_source" + const val HEADER_ATTRIBUTES_CLICK = "header_attributes_click" + const val HEADER_VARIANTS_CLICK = "header_variants_click" + const val MODULE_OPERATION_PROVIDER_CLASS = "module_operation_provider_class" + const val PACKAGE_ID = "package_id" + const val PACKAGE_VERSION = "package_version" + const val PACKAGE_FROM_VERSION = "package_from_version" + const val PACKAGE_FROM_SCOPE = "package_from_scope" + const val PACKAGE_TO_SCOPE = "package_to_scope" + const val REPOSITORY_ID = "repository_id" + const val REPOSITORY_URL = "repository_url" + const val REPOSITORY_USES_CUSTOM_URL = "repository_uses_custom_url" + const val PACKAGE_IS_INSTALLED = "package_is_installed" + const val TARGET_MODULES = "target_modules" + const val TARGET_MODULES_MIXED_BUILD_SYSTEMS = "target_modules_mixed_build_systems" + + const val PREFERENCES_GRADLE_SCOPES_COUNT = "preferences_gradle_scopes_count" + const val PREFERENCES_UPDATE_SCOPES_ON_USAGE = "preferences_update_scopes_on_usage" + const val PREFERENCES_DEFAULT_GRADLE_SCOPE_CHANGED = "preferences_default_gradle_scope_changed" + const val PREFERENCES_DEFAULT_MAVEN_SCOPE_CHANGED = "preferences_default_maven_scope_changed" + const val PREFERENCES_AUTO_ADD_REPOSITORIES = "preferences_auto_add_repositories" + const val DETAILS_LINK_LABEL = "details_link_label" + const val CHECKBOX_NAME = "checkbox_name" + const val CHECKBOX_STATE = "checkbox_state" + const val SEARCH_QUERY_LENGTH = "search_query_length" + const val SORT_METRIC = "sort_metric" + + enum class DetailsLinkTypes { Scm, Documentation, License, ProjectWebsite, Readme } + + enum class IndexedRepositories(val ids: Set, val urls: Set) { + OTHER(ids = emptySet(), urls = emptySet()), + NONE(ids = emptySet(), urls = emptySet()), + MAVEN_CENTRAL( + ids = setOf("maven_central"), + urls = setOf( + "https://repo.maven.apache.org/maven2/", + "https://maven-central.storage-download.googleapis.com/maven2", + "https://repo1.maven.org/maven2/" + ) + ), + GOOGLE_MAVEN( + ids = setOf("gmaven"), + urls = setOf("https://maven.google.com/") + ), + JETBRAINS_REPOS( + ids = setOf("dokka_dev", "compose_dev", "ktor_eap", "space_sdk"), + urls = setOf( + "https://maven.pkg.jetbrains.space/kotlin/p/dokka/dev/", + "https://maven.pkg.jetbrains.space/public/p/compose/dev/", + "https://maven.pkg.jetbrains.space/public/p/ktor/eap/", + "https://maven.pkg.jetbrains.space/public/p/space/maven/" + ) + ), + CLOJARS( + ids = setOf("clojars"), + urls = setOf("https://repo.clojars.org/") + ); + + companion object { + + fun forId(repositoryId: String?): IndexedRepositories { + if (repositoryId.isNullOrBlank()) return NONE + return entries.find { repositoryId in it.ids } ?: OTHER + } + + fun validateUrl(repositoryUrl: String?): String? { + if (repositoryUrl.isNullOrBlank()) return null + return if (repositoryUrl in indexedRepositoryUrls) repositoryUrl else null + } + } + } + + val indexedRepositoryUrls = IndexedRepositories.entries + .flatMap { it.urls } + + // EVENTS + const val PACKAGE_INSTALLED = "package_installed" + const val PACKAGE_REMOVED = "package_removed" + const val PACKAGE_VERSION_UPDATED = "package_version_updated" + const val PACKAGE_SCOPE_UPDATED = "package_scope_updated" + const val PACKAGE_VARIANT_UPDATED = "package_variant_updated" + const val REPOSITORY_ADDED = "repository_added" + const val REPOSITORY_REMOVED = "repository_removed" + const val PREFERENCES_CHANGED = "preferences_changed" + const val PREFERENCES_RESTORE_DEFAULTS = "preferences_restore_defaults" + const val PACKAGE_SELECTED = "package_selected" + const val MODULES_SELECTED = "modules_selected" + const val DETAILS_LINK_CLICK = "details_link_click" + const val TOGGLE = "toggle" + const val SORT_METRIC_CHANGED = "sort_metric_changed" + const val SEARCH_REQUEST = "search_request" + const val SEARCH_QUERY_CLEAR = "search_query_clear" + const val UPGRADE_ALL = "upgrade_all_event" + + // VALIDATORS + const val RULE_TOP_PACKAGE_ID = "top_package_id" +} diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/fus/PackageSearchEventsLogger.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/fus/PackageSearchEventsLogger.kt new file mode 100644 index 00000000..7534a03d --- /dev/null +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/fus/PackageSearchEventsLogger.kt @@ -0,0 +1,287 @@ +/******************************************************************************* + * Copyright 2000-2022 JetBrains s.r.o. and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +@file:Suppress("UnstableApiUsage") + +package com.jetbrains.packagesearch.plugin.fus + +import com.intellij.internal.statistic.eventLog.EventLogGroup +import com.intellij.internal.statistic.eventLog.events.BaseEventId +import com.intellij.internal.statistic.eventLog.events.EventFields +import com.intellij.internal.statistic.eventLog.validator.rules.impl.LocalFileCustomValidationRule +import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector +import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments +import com.jetbrains.packagesearch.plugin.PackageSearchBundle +import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModule +import com.jetbrains.packagesearch.plugin.utils.logError +import org.jetbrains.idea.reposearch.statistics.TopPackageIdValidationRule +import org.jetbrains.packagesearch.api.v3.ApiRepository + +private const val FUS_ENABLED = true + +internal class PackageSearchEventsLogger : CounterUsagesCollector() { + override fun getGroup() = GROUP +} + +private const val VERSION = 13 + +private val GROUP = EventLogGroup(FUSGroupIds.GROUP_ID, VERSION) + +internal class TopScopesValidationRule : LocalFileCustomValidationRule( + /* ruleId = */ "top_scopes_id", + /* resource = */ TopScopesValidationRule::class.java, + /* path = */ "/fus/scopes.txt" +) + +// FIELDS +private val buildSystemField = EventFields.Class(FUSGroupIds.MODULE_OPERATION_PROVIDER_CLASS) +private val packageIdField = + EventFields.StringValidatedByCustomRule(FUSGroupIds.PACKAGE_ID, TopPackageIdValidationRule::class.java) +private val packageVersionField = + EventFields.StringValidatedByRegexp(FUSGroupIds.PACKAGE_VERSION, regexpRef = "version") +private val packageFromVersionField = + EventFields.StringValidatedByRegexp(FUSGroupIds.PACKAGE_FROM_VERSION, regexpRef = "version") +private val packageScopeFromField = + EventFields.StringValidatedByCustomRule(FUSGroupIds.PACKAGE_FROM_SCOPE) +private val packageScopeToField = + EventFields.StringValidatedByCustomRule(FUSGroupIds.PACKAGE_TO_SCOPE) +private val repositoryIdField = EventFields.Enum(FUSGroupIds.REPOSITORY_ID) +private val repositoryUrlField = + EventFields.String(FUSGroupIds.REPOSITORY_URL, allowedValues = FUSGroupIds.indexedRepositoryUrls) +private val repositoryUsesCustomUrlField = EventFields.Boolean(FUSGroupIds.REPOSITORY_USES_CUSTOM_URL) +private val packageIsInstalledField = EventFields.Boolean(FUSGroupIds.PACKAGE_IS_INSTALLED) +private val targetModulesCountField = EventFields.Int(FUSGroupIds.TARGET_MODULES) +private val targetModulesMixedBuildSystemsField = + EventFields.Boolean(FUSGroupIds.TARGET_MODULES_MIXED_BUILD_SYSTEMS) + +private val preferencesGradleScopeCountField = EventFields.Int(FUSGroupIds.PREFERENCES_GRADLE_SCOPES_COUNT) +private val preferencesUpdateScopesOnUsageField = EventFields.Boolean(FUSGroupIds.PREFERENCES_UPDATE_SCOPES_ON_USAGE) +private val preferencesDefaultGradleScopeChangedField = + EventFields.Boolean(FUSGroupIds.PREFERENCES_DEFAULT_GRADLE_SCOPE_CHANGED) +private val preferencesDefaultMavenScopeChangedField = + EventFields.Boolean(FUSGroupIds.PREFERENCES_DEFAULT_MAVEN_SCOPE_CHANGED) +private val preferencesAutoAddRepositoriesField = + EventFields.Boolean(FUSGroupIds.PREFERENCES_AUTO_ADD_REPOSITORIES) +private val detailsLinkLabelField = + EventFields.Enum(FUSGroupIds.DETAILS_LINK_LABEL) +private val toggleValueField = EventFields.Boolean(FUSGroupIds.CHECKBOX_STATE) +private val searchQueryLengthField = EventFields.Int(FUSGroupIds.SEARCH_QUERY_LENGTH) +private val isSearchHeader = EventFields.Boolean(FUSGroupIds.IS_SEARCH_HEADER) + +// EVENTS +private val packageInstalledEvent = GROUP.registerEvent( + eventId = FUSGroupIds.PACKAGE_INSTALLED, + eventField1 = packageIdField, + eventField2 = buildSystemField +) +private val packageRemovedEvent = GROUP.registerEvent( + eventId = FUSGroupIds.PACKAGE_REMOVED, + eventField1 = packageIdField, + eventField2 = packageVersionField, + eventField3 = buildSystemField +) +private val packageVersionChangedEvent = GROUP.registerVarargEvent( + eventId = FUSGroupIds.PACKAGE_VERSION_UPDATED, + packageIdField, packageFromVersionField, packageVersionField, buildSystemField +) + +private val packageScopeChangedEvent = GROUP.registerVarargEvent( + eventId = FUSGroupIds.PACKAGE_SCOPE_UPDATED, + packageIdField, packageScopeFromField, packageScopeToField, buildSystemField +) + +private val packageVariantChangedEvent = GROUP.registerEvent( + eventId = FUSGroupIds.PACKAGE_VARIANT_UPDATED, + eventField1 = packageIdField, + eventField2 = buildSystemField +) +private val repositoryAddedEvent = GROUP.registerEvent( + eventId = FUSGroupIds.REPOSITORY_ADDED, + eventField1 = repositoryIdField, + eventField2 = repositoryUrlField +) +private val repositoryRemovedEvent = GROUP.registerEvent( + eventId = FUSGroupIds.REPOSITORY_REMOVED, + eventField1 = repositoryIdField, + eventField2 = repositoryUrlField, + eventField3 = repositoryUsesCustomUrlField +) +private val preferencesRestoreDefaultsEvent = GROUP.registerEvent(FUSGroupIds.PREFERENCES_RESTORE_DEFAULTS) +private val packageSelectedEvent = + GROUP.registerEvent(eventId = FUSGroupIds.PACKAGE_SELECTED, packageIsInstalledField) +private val targetModulesSelectedEvent = GROUP.registerEvent( + eventId = FUSGroupIds.MODULES_SELECTED, + eventField1 = targetModulesCountField, + eventField2 = targetModulesMixedBuildSystemsField +) +private val detailsLinkClickEvent = GROUP.registerEvent( + eventId = FUSGroupIds.DETAILS_LINK_CLICK, + eventField1 = detailsLinkLabelField +) +private val onlyStableToggleEvent = GROUP.registerEvent( + eventId = FUSGroupIds.TOGGLE, + eventField1 = toggleValueField, +) +private val searchRequestEvent = GROUP.registerEvent( + eventId = FUSGroupIds.SEARCH_REQUEST, + eventField1 = searchQueryLengthField +) +private val searchQueryClearEvent = GROUP.registerEvent(FUSGroupIds.SEARCH_QUERY_CLEAR) +private val upgradeAllEvent = GROUP.registerEvent(FUSGroupIds.UPGRADE_ALL) +private val infoPanelOpenedEvent = GROUP.registerEvent(FUSGroupIds.PANEL_OPENED) +private val goToSourceEvent = GROUP.registerEvent( + eventId = FUSGroupIds.GO_TO_SOURCE, + eventField1 = buildSystemField, + eventField2 = packageIdField +) +private val headerAttributesClick = GROUP.registerEvent( + FUSGroupIds.HEADER_ATTRIBUTES_CLICK, + eventField1 = isSearchHeader +) +private val headerVariantClick = GROUP.registerEvent(FUSGroupIds.HEADER_VARIANTS_CLICK) + +internal fun logPackageInstalled( + packageIdentifier: String, + targetModule: PackageSearchModule, +) = runSafelyIfEnabled(packageInstalledEvent) { + log(packageIdentifier, targetModule::class.java) +} + +internal fun logPackageRemoved( + packageIdentifier: String, + packageVersion: String?, + targetModule: PackageSearchModule, +) = runSafelyIfEnabled(packageRemovedEvent) { + log(packageIdentifier, packageVersion, targetModule::class.java) +} + +internal fun logPackageVersionChanged( + packageIdentifier: String, + packageFromVersion: String?, + packageTargetVersion: String, + targetModule: PackageSearchModule, +) = runSafelyIfEnabled(packageVersionChangedEvent) { + log( + packageIdField.with(packageIdentifier), + packageFromVersionField.with(packageFromVersion), + packageVersionField.with(packageTargetVersion), + buildSystemField.with(targetModule::class.java) + ) +} + +internal fun logPackageVariantChanged( + packageIdentifier: String, + targetModule: PackageSearchModule, +) = runSafelyIfEnabled(packageVariantChangedEvent) { + log(packageIdentifier, targetModule::class.java) +} + +internal fun logPackageScopeChanged( + packageIdentifier: String, + scopeFrom: String?, + scopeTo: String?, + targetModule: PackageSearchModule, +) = runSafelyIfEnabled(packageScopeChangedEvent) { + log( + packageIdField.with(packageIdentifier), + packageScopeFromField.with(scopeFrom ?: "[default]"), + packageScopeToField.with(scopeTo ?: "[default]"), + buildSystemField.with(targetModule::class.java) + ) +} + +internal fun logRepositoryAdded(model: ApiRepository) = runSafelyIfEnabled(repositoryAddedEvent) { + log(FUSGroupIds.IndexedRepositories.forId(model.id), FUSGroupIds.IndexedRepositories.validateUrl(model.url)) +} + +internal fun logRepositoryRemoved(model: ApiRepository) = runSafelyIfEnabled(repositoryRemovedEvent) { + val repository = FUSGroupIds.IndexedRepositories.forId(model.id) + val validatedUrl = FUSGroupIds.IndexedRepositories.validateUrl(model.url) + val usesCustomUrl = repository != FUSGroupIds.IndexedRepositories.NONE && + repository != FUSGroupIds.IndexedRepositories.OTHER && + validatedUrl == null + log(repository, validatedUrl, usesCustomUrl) +} + +internal fun logPreferencesRestoreDefaults() = runSafelyIfEnabled(preferencesRestoreDefaultsEvent) { + log() +} + +internal fun logTargetModuleSelected(targetModules: List) = + runSafelyIfEnabled(targetModulesSelectedEvent) { + if (targetModules.isNotEmpty()) { + log(targetModules.size, targetModules.groupBy { it.identity.group }.keys.size != 1) + } + } + +internal fun logPackageSelected(isInstalled: Boolean) = runSafelyIfEnabled(packageSelectedEvent) { + log(isInstalled) +} + +internal fun logDetailsLinkClick(type: FUSGroupIds.DetailsLinkTypes) = runSafelyIfEnabled(detailsLinkClickEvent) { + log(type) +} + +internal fun logOnlyStableToggle(state: Boolean) = runSafelyIfEnabled(onlyStableToggleEvent) { + log(state) +} + +internal fun logSearchRequest(query: String) = runSafelyIfEnabled(searchRequestEvent) { + log(query.length) +} + +internal fun logSearchQueryClear() = runSafelyIfEnabled(searchQueryClearEvent) { + log() +} + +internal fun logUpgradeAll() = runSafelyIfEnabled(upgradeAllEvent) { + log() +} + +internal fun logInfoPanelOpened() = runSafelyIfEnabled(infoPanelOpenedEvent) { + log() +} + +internal fun logGoToSource( + module: PackageSearchModule, + packageId: String, +) = runSafelyIfEnabled(goToSourceEvent) { + log(module::class.java, packageId) +} + +internal fun logHeaderAttributesClick(isSearchHeader: Boolean) = runSafelyIfEnabled(headerAttributesClick) { + log(isSearchHeader) +} + +internal fun logHeaderVariantsClick() = runSafelyIfEnabled(headerVariantClick) { + log() +} + +private fun runSafelyIfEnabled(event: T, action: T.() -> Unit) { + if (FUS_ENABLED) { + try { + event.action() + } catch (e: RuntimeException) { + logError( + message = PackageSearchBundle.message("packagesearch.logging.error", event.eventId), + throwable = RuntimeExceptionWithAttachments( + "Non-critical error while logging analytics event. This doesn't impact plugin functionality.", + e + ) + ) + } + } +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/services/PackageSearchApplicationCachesService.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/services/PackageSearchApplicationCachesService.kt index ac383374..44a81b6b 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/services/PackageSearchApplicationCachesService.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/services/PackageSearchApplicationCachesService.kt @@ -23,13 +23,11 @@ import com.jetbrains.packagesearch.plugin.utils.ApiSearchEntry import com.jetbrains.packagesearch.plugin.utils.KtorDebugLogger import com.jetbrains.packagesearch.plugin.utils.PackageSearchApiPackageCache import com.jetbrains.packagesearch.plugin.utils.PackageSearchProjectService -import com.jetbrains.packagesearch.plugin.utils.timer import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logging import java.util.concurrent.CompletableFuture import kotlin.io.path.absolutePathString import kotlin.io.path.div -import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -45,23 +43,29 @@ import org.jetbrains.packagesearch.api.v3.http.PackageSearchApiClient import org.jetbrains.packagesearch.api.v3.http.PackageSearchEndpoints @Service(Level.APP) -class PackageSearchApplicationCachesService(private val coroutineScope: CoroutineScope) : Disposable, RecoveryAction { +class PackageSearchApplicationCachesService : RecoveryAction, Disposable { + + + private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob()) companion object { private val cacheFilePath - get() = appSystemDir / "caches" / "packagesearch" / "db-${PackageSearch.pluginVersion}.db" + get() = cacheDirectory / "db-${PackageSearch.pluginVersion}.db" + + private val cacheDirectory + get() = appSystemDir / "caches" / "packagesearch" } @PKGSInternalAPI val cache = buildDefaultNitrate( - path = appSystemDir - .resolve(cacheFilePath) + path = cacheFilePath .apply { parent.toFile().mkdirs() } .absolutePathString() ) override fun dispose() { cache.close() + coroutineScope.cancel() } private inline fun getRepository(key: String) = @@ -79,22 +83,24 @@ class PackageSearchApplicationCachesService(private val coroutineScope: Coroutin private val repositoryCache get() = getRepository("repositories") - private val devApiClient = PackageSearchApiClient( - endpoints = PackageSearchEndpoints.DEV, + private val apiClient = PackageSearchApiClient( + endpoints = PackageSearchEndpoints.DEFAULT, httpClient = PackageSearchApiClient.defaultHttpClient { install(Logging) { level = LogLevel.ALL logger = KtorDebugLogger() filter { it.attributes.getOrNull(PackageSearchApiClient.Attributes.Cache) == true } } - }, - scope = coroutineScope + } ) + val isOnlineFlow = apiClient.isOnlineFlow() + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), true) + val apiPackageCache = PackageSearchApiPackageCache( apiPackageCache = packagesRepository, searchCache = searchesRepository, - apiClient = devApiClient + apiClient = apiClient ) private suspend fun createIndexes() { diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/services/PackageSearchProjectService.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/services/PackageSearchProjectService.kt index 6ebf2c54..2c25624b 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/services/PackageSearchProjectService.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/services/PackageSearchProjectService.kt @@ -17,9 +17,11 @@ import com.jetbrains.packagesearch.plugin.core.utils.PackageSearchProjectCachesS import com.jetbrains.packagesearch.plugin.core.utils.fileOpenedFlow import com.jetbrains.packagesearch.plugin.core.utils.replayOn import com.jetbrains.packagesearch.plugin.core.utils.withInitialValue +import com.jetbrains.packagesearch.plugin.fus.logOnlyStableToggle import com.jetbrains.packagesearch.plugin.utils.PackageSearchApplicationCachesService import com.jetbrains.packagesearch.plugin.utils.WindowedModuleBuilderContext import com.jetbrains.packagesearch.plugin.utils.filterNotNullKeys +import com.jetbrains.packagesearch.plugin.utils.logWarn import com.jetbrains.packagesearch.plugin.utils.nativeModulesFlow import com.jetbrains.packagesearch.plugin.utils.startWithNull import com.jetbrains.packagesearch.plugin.utils.timer @@ -33,6 +35,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.debounce @@ -45,13 +48,15 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.jetbrains.packagesearch.api.v3.ApiRepository @Service(Level.PROJECT) -class PackageSearchProjectService( - override val project: Project, - override val coroutineScope: CoroutineScope, -) : PackageSearchKnownRepositoriesContext { +class PackageSearchProjectService(override val project: Project) : PackageSearchKnownRepositoriesContext, Disposable { + + override val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob()) + private val restartChannel = Channel() @@ -97,7 +102,7 @@ class PackageSearchProjectService( ) { nativeModules, transformerExtensions, context -> transformerExtensions.flatMap { transformer -> nativeModules.map { module -> - with (context) { + with(context) { transformer.provideModule(module).startWithNull() } } @@ -109,9 +114,25 @@ class PackageSearchProjectService( private val restartFlow = restartChannel.consumeAsFlow() .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) + private var counter = 0 + private val counterMutex = Mutex() + + private suspend fun restartWithCounter() = counterMutex.withLock { + if (counter++ < 3) { + restart() + } + } + + private suspend fun resetCounter() = counterMutex.withLock { counter = 0 } + val modulesStateFlow = restartFlow .withInitialValue(Unit) .flatMapLatest { moduleProvidersList } + .catch { + logWarn("${this::class.simpleName}#modulesStateFlow", throwable = it) + restartWithCounter() + } + .onEach { resetCounter() } .stateIn(coroutineScope, SharingStarted.Eagerly, emptyList()) val modulesByBuildFile = modulesStateFlow @@ -124,8 +145,11 @@ class PackageSearchProjectService( init { + stableOnlyStateFlow + .onEach { logOnlyStableToggle(it) } + .launchIn(coroutineScope) + IntelliJApplication.PackageSearchApplicationCachesService - .apiPackageCache .isOnlineFlow .filter { it } .onEach { restart() } @@ -151,6 +175,9 @@ class PackageSearchProjectService( .launchIn(coroutineScope) } + override fun dispose() { + coroutineScope.cancel() + } } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/NoModulesFound.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/NoModulesFound.kt index 0b95cc80..e91db084 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/NoModulesFound.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/NoModulesFound.kt @@ -36,9 +36,9 @@ fun NoModulesFound( if (hasExternalProjects) { Row { LabelInfo("Try ") - val isEnabled by viewModel.isRefreshing.collectAsState() + val isRefreshing by viewModel.isRefreshing.collectAsState() Link( - enabled = isEnabled, + enabled = !isRefreshing, text = "refreshing", onClick = { viewModel.refreshExternalProjects() }, ) diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchColors.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchColors.kt new file mode 100644 index 00000000..01beec23 --- /dev/null +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchColors.kt @@ -0,0 +1,17 @@ +package com.jetbrains.packagesearch.plugin.ui + +import androidx.compose.ui.graphics.Color +import com.jetbrains.packagesearch.plugin.ui.bridge.pickComposeColorFromLaf + +object PackageSearchColors { + object Backgrounds { + fun packageItemHeader(): Color = + pickComposeColorFromLaf("ToolWindow.HeaderTab.selectedInactiveBackground") + + + fun attributeBadge() = packageItemHeader() + + } + + +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchMetrics.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchMetrics.kt index 3ae971c9..d7b11bb0 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchMetrics.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchMetrics.kt @@ -1,10 +1,32 @@ package com.jetbrains.packagesearch.plugin.ui import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.theme.scrollbarStyle object PackageSearchMetrics { + + val scrollbarWidth: Dp + @Composable + get() { + val metrics = JewelTheme.scrollbarStyle.metrics + return metrics.thumbThickness + + metrics.trackPadding.calculateEndPadding(LocalLayoutDirection.current) + } + + object Splitpane { + + val minWidth: Dp = 300.dp + const val firstSplitterPositionPercentage = .20f + + const val secondSplittePositionPercentage = .80f + } + object Popups { val minWidth: Dp = 50.dp @@ -14,9 +36,9 @@ object PackageSearchMetrics { val maxHeight: Dp = 250.dp } - object PackageList{ - object Item{ - val height= 24.dp + object PackageList { + object Item { + val height = 24.dp val padding = 8.dp } } @@ -24,7 +46,7 @@ object PackageSearchMetrics { val searchBarHeight = 36.dp val treeActionsHeight = searchBarHeight - object Dropdown{ + object Dropdown { val maxHeight = 100.dp } @@ -40,5 +62,4 @@ object PackageSearchMetrics { } } } - -} \ No newline at end of file +} diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchPackagePanel.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchPackagePanel.kt index 7b7165c2..ef36d5ec 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchPackagePanel.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchPackagePanel.kt @@ -1,36 +1,55 @@ package com.jetbrains.packagesearch.plugin.ui +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import com.intellij.ui.JBColor import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModule -import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem +import com.jetbrains.packagesearch.plugin.ui.bridge.packageSearchSplitter +import com.jetbrains.packagesearch.plugin.ui.model.ToolWindowViewModel import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItemEvent import com.jetbrains.packagesearch.plugin.ui.panels.packages.PackageSearchCentralPanel import com.jetbrains.packagesearch.plugin.ui.panels.side.PackageSearchInfoPanel import com.jetbrains.packagesearch.plugin.ui.panels.tree.PackageSearchModulesTree -import org.jetbrains.jewel.foundation.lazy.SelectableLazyListState -import org.jetbrains.jewel.ui.component.HorizontalSplitLayout +import org.jetbrains.compose.splitpane.HorizontalSplitPane +import org.jetbrains.jewel.bridge.toComposeColor +import org.jetbrains.jewel.foundation.theme.JewelTheme @Composable fun PackageSearchPackagePanel( onSelectionModulesSelectionChanged: (Set) -> Unit, isInfoPanelOpen: Boolean, onLinkClick: (String) -> Unit, - onPackageEvent:(PackageListItemEvent) -> Unit, + onPackageEvent: (PackageListItemEvent) -> Unit, ) { - HorizontalSplitLayout( - first = { PackageSearchModulesTree(it, onSelectionModulesSelectionChanged) }, - second = { + val toolWindowsViewModel = viewModel() + + val splitPaneState by remember { toolWindowsViewModel.firstSplitPaneState } + val innerSplitPaneState by remember { toolWindowsViewModel.secondSplitPaneState } + val splitterColor by remember(JewelTheme.isDark) { mutableStateOf(JBColor.border().toComposeColor()) } + + HorizontalSplitPane(Modifier.fillMaxSize(), splitPaneState) { + first(PackageSearchMetrics.Splitpane.minWidth) { + PackageSearchModulesTree(Modifier, onSelectionModulesSelectionChanged) + } + packageSearchSplitter(splitterColor) + second { if (isInfoPanelOpen) { - HorizontalSplitLayout( - modifier = it, - initialDividerPosition = 700.dp, - first = { PackageSearchCentralPanel(it, onLinkClick) }, - second = { PackageSearchInfoPanel(it, onLinkClick, onPackageEvent) } - ) - } else PackageSearchCentralPanel(it, onLinkClick) + HorizontalSplitPane(Modifier.fillMaxSize(), innerSplitPaneState) { + first(PackageSearchMetrics.Splitpane.minWidth) { + PackageSearchCentralPanel(onLinkClick = onLinkClick) + } + packageSearchSplitter(splitterColor) + second(PackageSearchMetrics.Splitpane.minWidth) { + PackageSearchInfoPanel(onLinkClick = onLinkClick, onPackageEvent = onPackageEvent) + } + } + + } else PackageSearchCentralPanel(onLinkClick = onLinkClick) } - ) + } } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/UiUtils.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/UiUtils.kt new file mode 100644 index 00000000..724cc5a3 --- /dev/null +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/UiUtils.kt @@ -0,0 +1,26 @@ +package com.jetbrains.packagesearch.plugin.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import com.intellij.openapi.project.Project +import com.jetbrains.packagesearch.plugin.ui.bridge.LocalPackageSearchDropdownLinkStyle +import com.jetbrains.packagesearch.plugin.ui.bridge.PackageSearchDropdownLinkStyle +import com.jetbrains.packagesearch.plugin.ui.bridge.PackageSearchGlobalColors +import com.jetbrains.packagesearch.plugin.ui.bridge.PackageSearchTabStyle +import com.jetbrains.packagesearch.plugin.ui.bridge.PackageSearchTreeStyle +import org.jetbrains.jewel.foundation.LocalGlobalColors +import org.jetbrains.jewel.ui.component.styling.LocalDefaultTabStyle +import org.jetbrains.jewel.ui.component.styling.LocalLazyTreeStyle + +@Composable +internal fun PackageSearchTheme(project: Project, content: @Composable () -> Unit) { + CompositionLocalProvider( + LocalComponentManager provides project, + LocalGlobalColors provides PackageSearchGlobalColors(), + LocalDefaultTabStyle provides PackageSearchTabStyle(), + LocalLazyTreeStyle provides PackageSearchTreeStyle(), + LocalPackageSearchDropdownLinkStyle provides PackageSearchDropdownLinkStyle(), + ) { + content() + } +} diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Components.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Components.kt index 59d18cd4..abe57e57 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Components.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Components.kt @@ -1,12 +1,20 @@ package com.jetbrains.packagesearch.plugin.ui.bridge +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.onClick +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -16,19 +24,20 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp import com.intellij.icons.AllIcons +import com.jetbrains.packagesearch.plugin.ui.PackageSearchColors import com.jetbrains.packagesearch.plugin.ui.PackageSearchMetrics +import java.awt.Cursor import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.foundation.theme.LocalTextStyle -import org.jetbrains.jewel.ui.component.Dropdown +import org.jetbrains.jewel.ui.component.DropdownLink import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.IconButton import org.jetbrains.jewel.ui.component.MenuScope import org.jetbrains.jewel.ui.component.PopupMenu import org.jetbrains.jewel.ui.component.Text -import org.jetbrains.jewel.ui.component.styling.DropdownColors -import org.jetbrains.jewel.ui.component.styling.DropdownStyle -import org.jetbrains.jewel.ui.theme.dropdownStyle +import org.jetbrains.compose.splitpane.SplitPaneScope @Composable fun LabelInfo( @@ -71,7 +80,7 @@ fun LabelInfo( @Composable -fun TextSelectionDropdown( +fun PackageSearchDropdownLink( modifier: Modifier, menuModifier: Modifier, items: List, @@ -79,11 +88,11 @@ fun TextSelectionDropdown( enabled: Boolean, onSelection: (String) -> Unit, ) { - Dropdown( + DropdownLink( modifier = modifier, menuModifier = menuModifier.heightIn(max = PackageSearchMetrics.Dropdown.maxHeight), enabled = enabled && items.isNotEmpty(), - style = packageSearchDropdownStyle(), + style = LocalPackageSearchDropdownLinkStyle.current, menuContent = { items.forEach { selectableItem( @@ -94,18 +103,11 @@ fun TextSelectionDropdown( } } }, - content = { - Text( - modifier = Modifier.align(Alignment.CenterEnd), - text = content, - maxLines = 1, - overflow = TextOverflow.Clip, - textAlign = TextAlign.End - ) - } + text = content ) } + @Composable internal fun PackageActionPopup( isOpen: Boolean, @@ -142,35 +144,73 @@ internal fun PackageActionPopup( } } + +internal fun SplitPaneScope.packageSearchSplitter( + splitterColor: Color, + cursor: PointerIcon = PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)), + hidden: Boolean = false, +) { + splitter { + visiblePart { + if (!hidden) { + Box( + modifier = Modifier + .fillMaxHeight() + .width(1.dp) + .background(splitterColor), + ) + } + } + handle { + if (!hidden) { + Box( + modifier = Modifier + .fillMaxHeight() + .width(8.dp) + .markAsHandle() + .pointerHoverIcon(cursor), + ) + } + } + } +} + + @Composable -private fun packageSearchDropdownStyle(): DropdownStyle { - val currentStyle = JewelTheme.dropdownStyle - return DropdownStyle( - colors = DropdownColors( - background = Color.Transparent, - backgroundDisabled = Color.Transparent, - backgroundFocused = Color.Transparent, - backgroundPressed = Color.Transparent, - backgroundHovered = Color.Transparent, - content = currentStyle.colors.content, - contentDisabled = currentStyle.colors.contentDisabled, - contentFocused = currentStyle.colors.contentFocused, - contentPressed = currentStyle.colors.contentPressed, - contentHovered = currentStyle.colors.contentHovered, - border = Color.Transparent, - borderDisabled = Color.Transparent, - borderFocused = Color.Transparent, - borderPressed = Color.Transparent, - borderHovered = Color.Transparent, - iconTintDisabled = Color.Transparent, - iconTint = currentStyle.colors.iconTint, - iconTintFocused = currentStyle.colors.iconTintFocused, - iconTintPressed = currentStyle.colors.iconTintPressed, - iconTintHovered = currentStyle.colors.iconTintHovered, - ), - metrics = currentStyle.metrics, - icons = currentStyle.icons, - textStyle = currentStyle.textStyle, - menuStyle = currentStyle.menuStyle, - ) -} \ No newline at end of file +internal fun AttributeBadge(text: String, onClick: () -> Unit) { + val isDark = JewelTheme.isDark + val background = remember(isDark) { + PackageSearchColors.Backgrounds.attributeBadge() + } + + Box( + modifier = Modifier + .background(color = background, shape = RoundedCornerShape(12.dp)) + .pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR))) + .onClick { onClick() }, + ) { + Text( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), text = text, + ) + } + +} + +//@Preview +//@Composable +//internal fun AttributeBadgePreview() { +// Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { +// IntUiTheme { +// Box(Modifier.background(LocalGlobalColors.current.paneBackground).padding(16.dp)) { +// AttributeBadge(text = "Android") {} +// } +// } +// IntUiTheme(true) { +// Box(Modifier.background(LocalGlobalColors.current.paneBackground).padding(16.dp)) { +// AttributeBadge(text = "Android") {} +// } +// } +// } +// +// +//} \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/CustomStyles.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/CustomStyles.kt new file mode 100644 index 00000000..26f20d1d --- /dev/null +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/CustomStyles.kt @@ -0,0 +1,113 @@ +package com.jetbrains.packagesearch.plugin.ui.bridge + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.dp +import com.jetbrains.packagesearch.plugin.ui.PackageSearchMetrics +import org.jetbrains.jewel.foundation.GlobalColors +import org.jetbrains.jewel.foundation.LocalGlobalColors +import org.jetbrains.jewel.foundation.OutlineColors +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.foundation.theme.LocalContentColor +import org.jetbrains.jewel.ui.component.styling.LazyTreeMetrics +import org.jetbrains.jewel.ui.component.styling.LazyTreeStyle +import org.jetbrains.jewel.ui.component.styling.LinkColors +import org.jetbrains.jewel.ui.component.styling.LinkStyle +import org.jetbrains.jewel.ui.component.styling.LocalDefaultTabStyle +import org.jetbrains.jewel.ui.component.styling.LocalLazyTreeStyle +import org.jetbrains.jewel.ui.component.styling.TabMetrics +import org.jetbrains.jewel.ui.component.styling.TabStyle +import org.jetbrains.jewel.ui.theme.linkStyle + + +@Composable +internal fun PackageSearchDropdownLinkStyle(): LinkStyle { + val currentStyle = JewelTheme.linkStyle + val contentColor = LocalContentColor.current + return LinkStyle( + colors = LinkColors( + content = contentColor, + contentDisabled = currentStyle.colors.contentDisabled, + contentHovered = contentColor, + contentPressed = contentColor, + contentFocused = contentColor, + contentVisited = contentColor, + ), + metrics = currentStyle.metrics, + icons = currentStyle.icons, + textStyles = currentStyle.textStyles, + ) +} + + +@Composable +internal fun PackageSearchTabStyle(): TabStyle { + val current = LocalDefaultTabStyle.current + return TabStyle( + colors = current.colors, + metrics = TabMetrics( + underlineThickness = current.metrics.underlineThickness, + tabPadding = current.metrics.tabPadding, + tabHeight = PackageSearchMetrics.searchBarHeight, + tabContentSpacing = 0.dp, + closeContentGap = current.metrics.closeContentGap, + ), + icons = current.icons, + contentAlpha = current.contentAlpha + ) +} + +@Composable +fun PackageSearchGlobalColors(): GlobalColors { + val colors = LocalGlobalColors.current + + return remember(colors) { + GlobalColors( + borders = colors.borders, + outlines = OutlineColors( + focused = Color.Transparent, + focusedWarning = colors.outlines.focusedWarning, + focusedError = colors.outlines.focusedError, + warning = colors.outlines.warning, + error = colors.outlines.error, + ), + infoContent = colors.infoContent, + paneBackground = colors.paneBackground, + ) + } +} + +@Composable +internal fun PackageSearchTreeStyle(): LazyTreeStyle { + val currentStyle = LocalLazyTreeStyle.current + val paddings = currentStyle.metrics.elementPadding + return LazyTreeStyle( + currentStyle.colors, + metrics = LazyTreeMetrics( + indentSize = currentStyle.metrics.indentSize, + elementPadding = PaddingValues( + top = paddings.calculateTopPadding(), + bottom = paddings.calculateBottomPadding(), + start = paddings.calculateStartPadding(LocalLayoutDirection.current), + end = 0.dp + ), + elementContentPadding = currentStyle.metrics.elementContentPadding, + elementMinHeight = currentStyle.metrics.elementMinHeight, + chevronContentGap = currentStyle.metrics.chevronContentGap, + elementBackgroundCornerSize = currentStyle.metrics.elementBackgroundCornerSize, + ), + currentStyle.icons, + ) +} + + +internal val LocalPackageSearchDropdownLinkStyle: ProvidableCompositionLocal = + staticCompositionLocalOf { + error("No PackageSearchDropdownLinkStyle provided. Have you forgotten the theme?") + } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Utils.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Utils.kt index 772738c9..a81e88f3 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Utils.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Utils.kt @@ -41,4 +41,3 @@ fun isLightTheme(): Boolean { private fun Color.getBrightness() = (red * 299 + green * 587 + blue * 114) / 1000 -fun Modifier.pointerChangeToHandModifier() = this.pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR))) diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/NoModulesFoundViewMode.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/NoModulesFoundViewMode.kt index 2e6caa08..b3ad63ec 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/NoModulesFoundViewMode.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/NoModulesFoundViewMode.kt @@ -1,27 +1,37 @@ package com.jetbrains.packagesearch.plugin.ui.model +import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service import com.intellij.openapi.externalSystem.ExternalSystemManager import com.intellij.openapi.externalSystem.importing.ImportSpecBuilder +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.project.ProjectData +import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId +import com.intellij.openapi.externalSystem.service.project.ExternalProjectRefreshCallback import com.intellij.openapi.externalSystem.util.ExternalSystemUtil import com.intellij.openapi.project.Project import com.jetbrains.packagesearch.plugin.core.utils.availableExtensionsFlow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @Service(Service.Level.PROJECT) -class NoModulesFoundViewMode( - private val project: Project, - private val viewModelScope: CoroutineScope, -) { - - val isRefreshing = project.isProjectSyncing - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) - +class NoModulesFoundViewMode(private val project: Project) : Disposable { + + private val viewModelScope: CoroutineScope = CoroutineScope(SupervisorJob()) + + private val isRefreshingChannel = Channel() + + val isRefreshing = isRefreshingChannel.consumeAsFlow() + .stateIn(viewModelScope, SharingStarted.Eagerly, false) + val hasExternalProjects = ExternalSystemManager.EP_NAME .availableExtensionsFlow .map { it.isNotEmpty() } @@ -30,13 +40,46 @@ class NoModulesFoundViewMode( started = SharingStarted.WhileSubscribed(), initialValue = ExternalSystemManager.EP_NAME.extensions.isNotEmpty() ) - + fun refreshExternalProjects() { + isRefreshingChannel.trySend(true) viewModelScope.launch(Dispatchers.Main) { ExternalSystemManager.EP_NAME.extensions - .map { ImportSpecBuilder(project, it.systemId) } + .map { + ImportSpecBuilder(project, it.systemId) + .callback(handleRefreshCallback()) + } .forEach { ExternalSystemUtil.refreshProjects(it) } } } - + + private fun handleRefreshCallback() = object : ExternalProjectRefreshCallback { + override fun onSuccess( + externalTaskId: ExternalSystemTaskId, + externalProject: DataNode?, + ) { + isRefreshingChannel.trySend(false) + } + + override fun onSuccess(externalProject: DataNode?) { + isRefreshingChannel.trySend(false) + } + + override fun onFailure( + externalTaskId: ExternalSystemTaskId, + errorMessage: String, + errorDetails: String?, + ) { + isRefreshingChannel.trySend(false) + } + + override fun onFailure(errorMessage: String, errorDetails: String?) { + isRefreshingChannel.trySend(false) + } + } + + override fun dispose() { + viewModelScope.cancel() + } + } \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/ToolWindowViewModel.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/ToolWindowViewModel.kt index c257deb4..1d555b4c 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/ToolWindowViewModel.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/ToolWindowViewModel.kt @@ -1,26 +1,48 @@ package com.jetbrains.packagesearch.plugin.ui.model +import com.intellij.openapi.Disposable +import androidx.compose.runtime.mutableStateOf import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service.Level import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.jetbrains.packagesearch.plugin.PackageSearchBundle.message import com.jetbrains.packagesearch.plugin.core.utils.smartModeFlow +import com.jetbrains.packagesearch.plugin.ui.PackageSearchMetrics import com.jetbrains.packagesearch.plugin.ui.bridge.openLinkInBrowser import com.jetbrains.packagesearch.plugin.ui.model.tree.TreeViewModel import com.jetbrains.packagesearch.plugin.utils.PackageSearchProjectService import kotlin.random.Random import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import org.jetbrains.compose.splitpane.SplitPaneState @Service(Level.PROJECT) -class ToolWindowViewModel(project: Project, private val viewModelScope: CoroutineScope) { +class ToolWindowViewModel(project: Project) : Disposable { + + private val viewModelScope: CoroutineScope = CoroutineScope(SupervisorJob()) + + + val firstSplitPaneState = mutableStateOf( + SplitPaneState( + initialPositionPercentage = PackageSearchMetrics.Splitpane.firstSplitterPositionPercentage, + moveEnabled = true, + ) + ) + val secondSplitPaneState = mutableStateOf( + SplitPaneState( + initialPositionPercentage = PackageSearchMetrics.Splitpane.secondSplittePositionPercentage, + moveEnabled = true, + ) + ) fun openLinkInBrowser(url: String) { viewModelScope.openLinkInBrowser(url) @@ -34,7 +56,7 @@ class ToolWindowViewModel(project: Project, private val viewModelScope: Coroutin val toolWindowState = combine( project.PackageSearchProjectService.packagesBeingDownloadedFlow, - project.isProjectSyncing, + project.isProjectImportingFlow, project.service() .tree .map { !it.isEmpty() } @@ -64,5 +86,9 @@ class ToolWindowViewModel(project: Project, private val viewModelScope: Coroutin started = SharingStarted.Lazily, initialValue = PackageSearchToolWindowState.Loading(message = easterEggMessage) ) + + override fun dispose() { + viewModelScope.cancel() + } } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/Utils.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/Utils.kt index 17a8c450..8fc1f628 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/Utils.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/Utils.kt @@ -21,7 +21,7 @@ internal fun PackageSearchDeclaredPackage.getLatestVersion(onlyStable: Boolean): } } -internal val Project.isProjectSyncing +internal val Project.isProjectImportingFlow get() = messageBus.flow(ProjectDataImportListener.TOPIC) { object : ProjectDataImportListener { override fun onImportStarted(projectPath: String?) { @@ -32,4 +32,4 @@ internal val Project.isProjectSyncing trySend(false) } } - }.withInitialValue(false) \ No newline at end of file + }.withInitialValue(false) diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContent.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContent.kt index 66d2afd9..d4004664 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContent.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContent.kt @@ -2,6 +2,7 @@ package com.jetbrains.packagesearch.plugin.ui.model.infopanel import com.jetbrains.packagesearch.plugin.core.data.IconProvider import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModule +import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModuleVariant import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem sealed interface InfoPanelContent { @@ -23,13 +24,14 @@ sealed interface InfoPanelContent { ) : Scm } data class Repository(val name: String, val url: String) + data class Type(val name: String, val icon: IconProvider.Icon) val packageListId: PackageListItem.Package.Id val moduleId: PackageSearchModule.Identity val title: String val subtitle: String val icon: IconProvider.Icon - val type: String? + val type: Type? val licenses: List val authors: List val description: String? @@ -55,7 +57,7 @@ sealed interface InfoPanelContent { override val title: String, override val subtitle: String, override val icon: IconProvider.Icon, - override val type: String, + override val type: Type?, override val licenses: List, override val authors: List, override val description: String?, @@ -78,7 +80,7 @@ sealed interface InfoPanelContent { override val title: String, override val subtitle: String, override val icon: IconProvider.Icon, - override val type: String, + override val type: Type?, override val licenses: List, override val authors: List, override val description: String?, @@ -107,7 +109,7 @@ sealed interface InfoPanelContent { override val title: String, override val subtitle: String, override val icon: IconProvider.Icon, - override val type: String, + override val type: Type, override val licenses: List, override val authors: List, override val description: String?, @@ -124,7 +126,7 @@ sealed interface InfoPanelContent { override val title: String, override val subtitle: String, override val icon: IconProvider.Icon, - override val type: String, + override val type: Type, override val licenses: List, override val authors: List, override val description: String?, @@ -139,4 +141,21 @@ sealed interface InfoPanelContent { } } + sealed interface Attributes : InfoPanelContent{ + val attributes: List + + data class FromVariant( + override val tabTitle: String, + val variantName: String, + override val attributes: List, + ) : Attributes + + data class FromSearch( + override val tabTitle: String, + override val attributes: List, + val defaultSourceSet: String, + val additionalSourceSets: List, + ) : Attributes + } + } \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContentEvent.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContentEvent.kt index 79f5a506..28da25d2 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContentEvent.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContentEvent.kt @@ -2,14 +2,14 @@ package com.jetbrains.packagesearch.plugin.ui.model.infopanel import com.jetbrains.packagesearch.plugin.core.data.PackageSearchDeclaredPackage import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModule +import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModuleVariant import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem import org.jetbrains.packagesearch.api.v3.ApiPackage sealed interface InfoPanelContentEvent { - val module: PackageSearchModule - sealed interface Package : InfoPanelContentEvent { + val module: PackageSearchModule val packageListId: PackageListItem.Package.Id @@ -51,5 +51,20 @@ sealed interface InfoPanelContentEvent { } } + sealed interface Attributes : InfoPanelContentEvent { + val attributes: List + + data class FromVariant( + val variantName: String, + override val attributes: List, + ) : Attributes + + data class FromSearch( + val defaultVariant: String, + val additionalVariants: List, + override val attributes: List, + ) : Attributes + + } } \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelViewModel.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelViewModel.kt index 0fb50cff..fe660eed 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelViewModel.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelViewModel.kt @@ -1,16 +1,20 @@ package com.jetbrains.packagesearch.plugin.ui.model.infopanel import androidx.compose.foundation.ScrollState +import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service.Level import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.jetbrains.packagesearch.plugin.core.data.PackageSearchDeclaredPackage import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModule +import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModuleVariant import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListViewModel import com.jetbrains.packagesearch.plugin.utils.PackageSearchProjectService import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -26,10 +30,9 @@ import kotlinx.coroutines.flow.stateIn import org.jetbrains.packagesearch.api.v3.ApiPackage @Service(Level.PROJECT) -class InfoPanelViewModel( - private val project: Project, - viewModelScope: CoroutineScope, -) { +class InfoPanelViewModel(private val project: Project) : Disposable { + + private val viewModelScope: CoroutineScope = CoroutineScope(SupervisorJob()) private val setDataEventChannel = Channel() @@ -62,8 +65,17 @@ class InfoPanelViewModel( } } } + + is InfoPanelContentEvent.Attributes.FromVariant -> { + event.asPanelContent() + } + + is InfoPanelContentEvent.Attributes.FromSearch -> { + event.asPanelContent() + } } - }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + } + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) private val activeTabTitleMutableStateFlow: MutableStateFlow = MutableStateFlow(null) val activeTabTitleFlow = activeTabTitleMutableStateFlow.asStateFlow() @@ -72,7 +84,6 @@ class InfoPanelViewModel( activeTabTitleMutableStateFlow.value = title } - init { combine( tabs.map { it.map { it.tabTitle } }, @@ -85,7 +96,6 @@ class InfoPanelViewModel( .launchIn(viewModelScope) } - fun setPackage( module: PackageSearchModule.Base, declaredPackage: PackageSearchDeclaredPackage, @@ -135,6 +145,35 @@ class InfoPanelViewModel( ) } + fun setDeclaredHeaderAttributes( + variantName: String, + attributes: List, + ) { + setDataEventChannel.trySend( + InfoPanelContentEvent.Attributes.FromVariant( + variantName = variantName, + attributes = attributes + ) + ) + } + + fun setSearchHeaderAttributes( + defaultVariant: String, + additionalVariants: List, + attributes: List, + ) { + setDataEventChannel.trySend( + InfoPanelContentEvent.Attributes.FromSearch( + defaultVariant = defaultVariant, + additionalVariants = additionalVariants, + attributes = attributes + ) + ) + } + + override fun dispose() { + viewModelScope.cancel() + } } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/Utils.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/Utils.kt index 3d237a05..b62d6d07 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/Utils.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/Utils.kt @@ -1,6 +1,7 @@ package com.jetbrains.packagesearch.plugin.ui.model.infopanel import com.jetbrains.packagesearch.plugin.PackageSearchBundle.message +import org.jetbrains.jewel.foundation.lazy.tree.buildTree import org.jetbrains.packagesearch.api.v3.ApiGitHub import org.jetbrains.packagesearch.api.v3.ApiScm import org.jetbrains.packagesearch.api.v3.LicenseFile @@ -24,4 +25,5 @@ internal fun Licenses<*>.asInfoPanelLicenseList() = buildList { internal fun LicenseFile.toInfoPanelLicense(): InfoPanelContent.PackageInfo.License? { val name = name ?: url ?: return null return InfoPanelContent.PackageInfo.License(name, url) -} \ No newline at end of file +} + diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/asPanelContent.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/asPanelContent.kt index 4042a86e..61b0f4e5 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/asPanelContent.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/asPanelContent.kt @@ -1,6 +1,8 @@ package com.jetbrains.packagesearch.plugin.ui.model.infopanel import com.jetbrains.packagesearch.plugin.PackageSearchBundle.message +import com.jetbrains.packagesearch.plugin.core.data.IconProvider +import com.jetbrains.packagesearch.plugin.core.data.PackageSearchDeclaredPackage import com.jetbrains.packagesearch.plugin.core.extensions.PackageSearchKnownRepositoriesContext import com.jetbrains.packagesearch.plugin.core.utils.icon import com.jetbrains.packagesearch.plugin.ui.model.getLatestVersion @@ -18,45 +20,66 @@ context(PackageSearchKnownRepositoriesContext) internal fun InfoPanelContentEvent.Package.Declared.Base.asPanelContent( onlyStable: Boolean, isLoading: Boolean, -) = - listOf( - InfoPanelContent.PackageInfo.Declared.Base( - moduleId = module.identity, - packageListId = packageListId, - tabTitle = message("packagesearch.ui.toolwindow.packages.details.info.overview"), - title = declaredPackage.displayName, - subtitle = declaredPackage.coordinates, - icon = declaredPackage.icon, - type = when (declaredPackage.remoteInfo) { +) = listOf( + InfoPanelContent.PackageInfo.Declared.Base( + moduleId = module.identity, + packageListId = packageListId, + tabTitle = message("packagesearch.ui.toolwindow.packages.details.info.overview"), + title = declaredPackage.displayName, + subtitle = declaredPackage.coordinates, + icon = declaredPackage.icon, + type = declaredPackage.typeInfo, + licenses = declaredPackage.remoteInfo?.licenses?.asInfoPanelLicenseList() ?: emptyList(), + authors = declaredPackage.remoteInfo?.authors?.mapNotNull { it.name } ?: emptyList(), + description = declaredPackage.remoteInfo + ?.description + ?.sanitizeDescription(), + scm = declaredPackage.remoteInfo?.scm?.asInfoPanelScm(), + readmeUrl = declaredPackage.remoteInfo?.scm?.readme?.htmlUrl ?: declaredPackage.remoteInfo?.scm?.readmeUrl, + repositories = declaredPackage.remoteInfo?.repositories() ?: emptyList(), + latestVersion = declaredPackage.getLatestVersion(onlyStable)?.versionName, + declaredVersion = declaredPackage.declaredVersion + ?.versionName + ?: message("packagesearch.ui.missingVersion"), + declaredScope = declaredPackage.declaredScope + ?: message("packagesearch.ui.missingScope"), + availableVersions = declaredPackage.remoteInfo + ?.versions + ?.all + ?.filter { if (onlyStable) it.normalizedVersion.isStable else true } + ?.map { it.normalizedVersion.versionName } + ?: emptyList(), + availableScopes = module.availableScopes, + isLoading = isLoading, + allowMissingScope = !module.dependencyMustHaveAScope + ) +) + +internal val PackageSearchDeclaredPackage.typeInfo: InfoPanelContent.PackageInfo.Type? + get() { + return InfoPanelContent.PackageInfo.Type( + name = when (remoteInfo) { is ApiMavenPackage -> message("packagesearch.configuration.maven.title") - null -> message("packagesearch.ui.toolwindow.packages.details.info.unknown") + null -> when (icon) { + IconProvider.Icons.MAVEN -> message("packagesearch.configuration.maven.title") + IconProvider.Icons.NPM -> message("packagesearch.configuration.npm.title") + IconProvider.Icons.COCOAPODS -> message("packagesearch.configuration.cocoapods.title") + else -> return null + } }, - licenses = declaredPackage.remoteInfo?.licenses?.asInfoPanelLicenseList() ?: emptyList(), - authors = declaredPackage.remoteInfo?.authors?.mapNotNull { it.name } ?: emptyList(), - description = declaredPackage.remoteInfo - ?.description - ?.sanitizeDescription(), - scm = declaredPackage.remoteInfo?.scm?.asInfoPanelScm(), - readmeUrl = declaredPackage.remoteInfo?.scm?.readmeUrl, - repositories = declaredPackage.remoteInfo?.repositories() ?: emptyList(), - latestVersion = declaredPackage.getLatestVersion(onlyStable)?.versionName, - declaredVersion = declaredPackage.declaredVersion - ?.versionName - ?: message("packagesearch.ui.missingVersion"), - declaredScope = declaredPackage.declaredScope - ?: message("packagesearch.ui.missingScope"), - availableVersions = declaredPackage.remoteInfo - ?.versions - ?.all - ?.filter { if (onlyStable) it.normalizedVersion.isStable else true } - ?.map { it.normalizedVersion.versionName } - ?: emptyList(), - availableScopes = module.availableScopes, - isLoading = isLoading, - allowMissingScope = !module.dependencyMustHaveAScope + icon = icon ) + } + +internal val ApiPackage.typeInfo: InfoPanelContent.PackageInfo.Type + get() = InfoPanelContent.PackageInfo.Type( + name = when (this) { + is ApiMavenPackage -> message("packagesearch.configuration.maven.title") + }, + icon = icon ) + private fun String.sanitizeDescription() = replace("\r\n", "\n") .replace("\r", "\n") @@ -68,76 +91,79 @@ context(PackageSearchKnownRepositoriesContext) internal fun InfoPanelContentEvent.Package.Declared.WithVariant.asPanelContent( onlyStable: Boolean, isLoading: Boolean, -) = - listOf( - InfoPanelContent.PackageInfo.Declared.WithVariant( - moduleId = module.identity, - packageListId = packageListId, - tabTitle = message("packagesearch.ui.toolwindow.packages.details.info.overview"), - title = declaredPackage.displayName, - subtitle = declaredPackage.coordinates, - icon = declaredPackage.icon, - type = when (declaredPackage.remoteInfo) { - is ApiMavenPackage -> message("packagesearch.configuration.maven.title") - null -> message("packagesearch.ui.toolwindow.packages.details.info.unknown") - }, - licenses = declaredPackage.remoteInfo?.licenses?.asInfoPanelLicenseList() ?: emptyList(), - authors = declaredPackage.remoteInfo?.authors?.mapNotNull { it.name } ?: emptyList(), - description = declaredPackage.remoteInfo - ?.description - ?.sanitizeDescription(), - scm = declaredPackage.remoteInfo?.scm?.asInfoPanelScm(), - readmeUrl = declaredPackage.remoteInfo?.scm?.readmeUrl, - repositories = declaredPackage.remoteInfo?.repositories() ?: emptyList(), - latestVersion = declaredPackage.getLatestVersion(onlyStable)?.versionName, - declaredVersion = declaredPackage.declaredVersion - ?.versionName - ?: message("packagesearch.ui.missingVersion"), - declaredScope = declaredPackage.declaredScope - ?: message("packagesearch.ui.missingScope"), - availableVersions = declaredPackage.remoteInfo - ?.versions - ?.all - ?.filter { if (onlyStable) it.normalizedVersion.isStable else true } - ?.map { it.normalizedVersion.versionName } - ?: emptyList(), - availableScopes = module.variants.getValue(variantName).availableScopes, - isLoading = isLoading, - compatibleVariants = module.variants.keys.sorted() - variantName, - declaredVariant = variantName, - allowMissingScope = !module.dependencyMustHaveAScope, - variantTerminology = module.variantTerminology - ) +) = listOf( + InfoPanelContent.PackageInfo.Declared.WithVariant( + moduleId = module.identity, + packageListId = packageListId, + tabTitle = message("packagesearch.ui.toolwindow.packages.details.info.overview"), + title = declaredPackage.displayName, + subtitle = declaredPackage.coordinates, + icon = declaredPackage.icon, + type = declaredPackage.typeInfo, + licenses = declaredPackage.remoteInfo?.licenses?.asInfoPanelLicenseList() ?: emptyList(), + authors = declaredPackage.remoteInfo?.authors?.mapNotNull { it.name } ?: emptyList(), + description = declaredPackage.remoteInfo + ?.description + ?.sanitizeDescription(), + scm = declaredPackage.remoteInfo?.scm?.asInfoPanelScm(), + readmeUrl = declaredPackage.remoteInfo?.scm?.readme?.htmlUrl ?: declaredPackage.remoteInfo?.scm?.readmeUrl, + repositories = declaredPackage.remoteInfo?.repositories() ?: emptyList(), + latestVersion = declaredPackage.getLatestVersion(onlyStable)?.versionName, + declaredVersion = declaredPackage.declaredVersion + ?.versionName + ?: message("packagesearch.ui.missingVersion"), + declaredScope = declaredPackage.declaredScope + ?: message("packagesearch.ui.missingScope"), + availableVersions = declaredPackage.remoteInfo + ?.versions + ?.all + ?.filter { if (onlyStable) it.normalizedVersion.isStable else true } + ?.map { it.normalizedVersion.versionName } + ?: emptyList(), + availableScopes = module.variants.getValue(variantName).availableScopes, + isLoading = isLoading, + compatibleVariants = module.variants.keys.sorted() - variantName, + declaredVariant = variantName, + allowMissingScope = !module.dependencyMustHaveAScope, + variantTerminology = module.variantTerminology + ), + InfoPanelContent.Attributes.FromVariant( + variantName = variantName, + tabTitle = message("packagesearch.ui.toolwindow.sidepanel.platforms"), + attributes = module.variants.getValue(variantName).attributes ) +) context(PackageSearchKnownRepositoriesContext) internal fun InfoPanelContentEvent.Package.Remote.WithVariants.asPanelContent( isLoading: Boolean, -) = - listOf( - InfoPanelContent.PackageInfo.Remote.WithVariant( - tabTitle = message("packagesearch.ui.toolwindow.packages.details.info.overview"), - moduleId = module.identity, - packageListId = packageListId, - title = apiPackage.name, - subtitle = apiPackage.coordinates, - icon = apiPackage.icon, - type = when (apiPackage) { - is ApiMavenPackage -> message("packagesearch.configuration.maven.title") - }, - licenses = apiPackage.licenses?.asInfoPanelLicenseList() ?: emptyList(), - authors = apiPackage.authors.mapNotNull { it.name }, - description = apiPackage.description?.sanitizeDescription(), - scm = apiPackage.scm?.asInfoPanelScm(), - readmeUrl = apiPackage.scm?.readmeUrl, - primaryVariant = primaryVariantName, - additionalVariants = compatibleVariantNames.sorted() - primaryVariantName, - repositories = apiPackage.repositories(), - isLoading = isLoading, - isInstalledInPrimaryVariant = module.variants.getValue(primaryVariantName).declaredDependencies - .any { it.id == apiPackage.id } - ) +) = listOf( + InfoPanelContent.PackageInfo.Remote.WithVariant( + tabTitle = message("packagesearch.ui.toolwindow.packages.details.info.overview"), + moduleId = module.identity, + packageListId = packageListId, + title = apiPackage.name, + subtitle = apiPackage.coordinates, + icon = apiPackage.icon, + type = apiPackage.typeInfo, + licenses = apiPackage.licenses?.asInfoPanelLicenseList() ?: emptyList(), + authors = apiPackage.authors.mapNotNull { it.name }, + description = apiPackage.description?.sanitizeDescription(), + scm = apiPackage.scm?.asInfoPanelScm(), + readmeUrl = apiPackage.scm?.readme?.htmlUrl ?: apiPackage.scm?.readmeUrl, + primaryVariant = primaryVariantName, + additionalVariants = compatibleVariantNames.sorted() - primaryVariantName, + repositories = apiPackage.repositories(), + isLoading = isLoading, + isInstalledInPrimaryVariant = module.variants.getValue(primaryVariantName).declaredDependencies + .any { it.id == apiPackage.id } + ), + InfoPanelContent.Attributes.FromVariant( + tabTitle = message("packagesearch.ui.toolwindow.sidepanel.platforms"), + variantName = primaryVariantName, + attributes = module.variants.getValue(primaryVariantName).attributes ) +) context(PackageSearchKnownRepositoriesContext) internal fun InfoPanelContentEvent.Package.Remote.Base.asPanelContent( @@ -150,15 +176,29 @@ internal fun InfoPanelContentEvent.Package.Remote.Base.asPanelContent( title = apiPackage.name, subtitle = apiPackage.coordinates, icon = apiPackage.icon, - type = when (apiPackage) { - is ApiMavenPackage -> message("packagesearch.configuration.maven.title") - }, + type = apiPackage.typeInfo, licenses = apiPackage.licenses?.asInfoPanelLicenseList() ?: emptyList(), authors = apiPackage.authors.mapNotNull { it.name }, description = apiPackage.description?.sanitizeDescription(), scm = apiPackage.scm?.asInfoPanelScm(), - readmeUrl = apiPackage.scm?.readmeUrl, + readmeUrl = apiPackage.scm?.readme?.htmlUrl ?: apiPackage.scm?.readmeUrl, repositories = apiPackage.repositories(), isLoading = isLoading ) +) + +internal fun InfoPanelContentEvent.Attributes.FromVariant.asPanelContent() = listOf( + InfoPanelContent.Attributes.FromVariant( + variantName = variantName, + tabTitle = message("packagesearch.ui.toolwindow.sidepanel.platforms"), + attributes = attributes, + ) +) +internal fun InfoPanelContentEvent.Attributes.FromSearch.asPanelContent() = listOf( + InfoPanelContent.Attributes.FromSearch( + tabTitle = message("packagesearch.ui.toolwindow.packages.details.info.attributes"), + defaultSourceSet = defaultVariant, + additionalSourceSets = additionalVariants, + attributes = attributes, + ) ) \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListBuilder.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListBuilder.kt index eb92c291..a432dab5 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListBuilder.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListBuilder.kt @@ -35,12 +35,8 @@ class PackageListBuilder( } private fun List.getUpdatesAvailableAdditionalContent() = - count { it.hasUpdates(onlyStable) } - .takeIf { it > 0 } - ?.let { PackageListItem.Header.AdditionalContent.UpdatesAvailableCount(it) } - - private fun PackageSearchModuleVariant.getUpdatesAvailableAdditionalContent() = - declaredDependencies.count { it.hasUpdates(onlyStable) } + filter { it.matchesSearchQuery() } + .count { it.hasUpdates(onlyStable) } .takeIf { it > 0 } ?.let { PackageListItem.Header.AdditionalContent.UpdatesAvailableCount(it) } @@ -81,18 +77,21 @@ class PackageListBuilder( val dependenciesToShow = base.declaredDependencies .filter { it.matchesSearchQuery() } val state = getStateForOrOpen(id) - addHeader( - title = base.name, - id = id, - count = base.declaredDependencies.size, - state = state, - additionalContent = when (id) { - in headerLoadingStates -> PackageListItem.Header.AdditionalContent.Loading - else -> dependenciesToShow.getUpdatesAvailableAdditionalContent() - } - ) + if (dependenciesToShow.isNotEmpty() || isCompact) { + addHeader( + title = base.name, + id = id, + state = state, + additionalContent = when (id) { + in headerLoadingStates -> PackageListItem.Header.AdditionalContent.Loading + else -> dependenciesToShow.getUpdatesAvailableAdditionalContent() + } + ) + } if (state == PackageListItem.Header.State.OPEN) { - dependenciesToShow.forEach { dependency -> + dependenciesToShow + .filter { it.matchesSearchQuery() } + .forEach { dependency -> addDeclaredPackage( title = dependency.displayName, subtitle = dependency.coordinates, @@ -142,7 +141,6 @@ class PackageListBuilder( title: String, id: PackageListItem.Header.Id, state: PackageListItem.Header.State, - count: Int?, attributes: List = emptyList(), additionalContent: PackageListItem.Header.AdditionalContent? = null, ) { @@ -151,8 +149,7 @@ class PackageListBuilder( title = title, id = id, state = state, - count = count, - attriutes = attributes, + attributes = attributes, additionalContent = additionalContent, ) ) @@ -184,16 +181,17 @@ class PackageListBuilder( moduleIdentity = module.identity, variantName = variant.name ), - count = variant.declaredDependencies.size, - attributes = variant.attributes.map { it.value }, state = state, + attributes = variant.attributes.map { it.value }, additionalContent = when (id) { in headerLoadingStates -> PackageListItem.Header.AdditionalContent.Loading - else -> variant.getUpdatesAvailableAdditionalContent() + else -> variant.declaredDependencies.getUpdatesAvailableAdditionalContent() } ) if (state == PackageListItem.Header.State.OPEN) { - variant.declaredDependencies.forEach { dependency -> + variant.declaredDependencies + .filter { it.matchesSearchQuery() } + .forEach { dependency -> addDeclaredPackage( title = dependency.displayName, subtitle = dependency.coordinates, @@ -225,7 +223,6 @@ class PackageListBuilder( addHeader( title = module.name, id = id, - count = dependenciesToShow.size, state = state, additionalContent = when (id) { in headerLoadingStates -> PackageListItem.Header.AdditionalContent.Loading @@ -234,7 +231,9 @@ class PackageListBuilder( } ) if (state == PackageListItem.Header.State.OPEN) { - dependenciesToShow.forEach { (variant, dependency) -> + dependenciesToShow + .filter { it.second.matchesSearchQuery() } + .forEach { (variant, dependency) -> addDeclaredPackage( title = dependency.displayName, subtitle = variant.name, @@ -264,8 +263,7 @@ class PackageListBuilder( state = when (headerCollapsedStates[headerId]) { TargetState.OPEN -> PackageListItem.Header.State.LOADING else -> PackageListItem.Header.State.CLOSED - }, - count = null + } ) is Search.Query.WithVariants -> addHeader( @@ -276,8 +274,7 @@ class PackageListBuilder( else -> PackageListItem.Header.State.CLOSED }, attributes = search.attributes, - additionalContent = search.buildVariantsText(), - count = null + additionalContent = search.buildVariantsText() ) is Search.Results.Base -> addFromSearchQueryBase( @@ -307,7 +304,6 @@ class PackageListBuilder( addHeader( title = PackageSearchBundle.message("packagesearch.ui.toolwindow.tab.packages.searchResults"), id = headerId, - count = search.packages.size, state = state, attributes = search.attributes, additionalContent = search.buildVariantsText(), @@ -360,7 +356,6 @@ class PackageListBuilder( addHeader( title = PackageSearchBundle.message("packagesearch.ui.toolwindow.tab.packages.searchResults"), id = headerId, - count = search.packages.size, state = state ) if (state == PackageListItem.Header.State.OPEN) { diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItem.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItem.kt index 72d631d9..177d8aab 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItem.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItem.kt @@ -18,8 +18,7 @@ sealed interface PackageListItem { override val title: String, override val id: Id, val state: State, - val count: Int? = null, - val attriutes: List = emptyList(), + val attributes: List = emptyList(), val additionalContent: AdditionalContent? = null, ) : PackageListItem { diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItemEvent.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItemEvent.kt index 1a0f27f7..62233de8 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItemEvent.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItemEvent.kt @@ -21,7 +21,19 @@ sealed interface PackageListItemEvent { sealed interface InfoPanelEvent : PackageListItemEvent { @Serializable - data class OnHeaderAttributesClick(override val eventId: PackageListItem.Header.Id) : InfoPanelEvent + sealed interface OnHeaderAttributesClick : InfoPanelEvent { + @Serializable + data class DeclaredHeaderAttributesClick( + override val eventId: PackageListItem.Header.Id.Declared, + val variantName: String, + ) : OnHeaderAttributesClick + @Serializable + data class SearchHeaderAttributesClick( + override val eventId: PackageListItem.Header.Id.Remote, + val attributesNames: List + ) : OnHeaderAttributesClick + + } @Serializable data class OnHeaderVariantsClick(override val eventId: PackageListItem.Header.Id) : InfoPanelEvent @@ -31,6 +43,11 @@ sealed interface PackageListItemEvent { @Serializable data class OnPackageDoubleClick(override val eventId: PackageListItem.Id) : InfoPanelEvent + + @Serializable + data class OnSelectedPackageClick(override val eventId: PackageListItem.Id) : InfoPanelEvent + + } @Serializable @@ -87,9 +104,6 @@ sealed interface PackageListItemEvent { } - - - @Serializable data class Update(override val eventId: PackageListItem.Package.Declared.Id) : OnPackageAction diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListViewModel.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListViewModel.kt index 5f234573..7e018f72 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListViewModel.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListViewModel.kt @@ -1,6 +1,7 @@ package com.jetbrains.packagesearch.plugin.ui.model.packageslist import androidx.compose.foundation.lazy.LazyListState +import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service.Level import com.intellij.openapi.components.service @@ -12,6 +13,20 @@ import com.jetbrains.packagesearch.plugin.core.data.PackageSearchDependencyManag import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModule import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModuleEditor import com.jetbrains.packagesearch.plugin.core.utils.IntelliJApplication +import com.jetbrains.packagesearch.plugin.fus.logGoToSource +import com.jetbrains.packagesearch.plugin.fus.logHeaderAttributesClick +import com.jetbrains.packagesearch.plugin.fus.logHeaderVariantsClick +import com.jetbrains.packagesearch.plugin.fus.logInfoPanelOpened +import com.jetbrains.packagesearch.plugin.fus.logPackageInstalled +import com.jetbrains.packagesearch.plugin.fus.logPackageRemoved +import com.jetbrains.packagesearch.plugin.fus.logPackageScopeChanged +import com.jetbrains.packagesearch.plugin.fus.logPackageSelected +import com.jetbrains.packagesearch.plugin.fus.logPackageVariantChanged +import com.jetbrains.packagesearch.plugin.fus.logPackageVersionChanged +import com.jetbrains.packagesearch.plugin.fus.logSearchQueryClear +import com.jetbrains.packagesearch.plugin.fus.logSearchRequest +import com.jetbrains.packagesearch.plugin.fus.logTargetModuleSelected +import com.jetbrains.packagesearch.plugin.fus.logUpgradeAll import com.jetbrains.packagesearch.plugin.ui.model.ToolWindowViewModel import com.jetbrains.packagesearch.plugin.ui.model.getLatestVersion import com.jetbrains.packagesearch.plugin.ui.model.hasUpdates @@ -26,6 +41,8 @@ import com.jetbrains.packagesearch.plugin.utils.searchPackages import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -35,9 +52,12 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -49,14 +69,13 @@ import org.jetbrains.packagesearch.api.v3.ApiPackage import org.jetbrains.packagesearch.api.v3.search.buildSearchParameters @Service(Level.PROJECT) -class PackageListViewModel( - private val project: Project, - private val viewModelScope: CoroutineScope, -) { +class PackageListViewModel(private val project: Project) : Disposable { + + private val viewModelScope: CoroutineScope = CoroutineScope(SupervisorJob()) + private val isOnline get() = IntelliJApplication.PackageSearchApplicationCachesService - .apiPackageCache .isOnlineFlow val isCompactFlow @@ -72,19 +91,31 @@ class PackageListViewModel( val isOnlineSearchEnabledFlow = combine(listOf(selectedModuleIdsSharedFlow.map { it.size == 1 }, isOnline)) { it.all { it } - }.stateIn(viewModelScope, SharingStarted.Eagerly, true) - - private val selectedModulesFlow - get() = combine( - selectedModuleIdsSharedFlow, - project.PackageSearchProjectService.modulesByIdentity - ) { selectedModules, modulesByIdentity -> - modulesByIdentity.filterKeys { it in selectedModules }.values.toList() } + .stateIn(viewModelScope, SharingStarted.Eagerly, true) + + private val selectedModulesFlow = combine( + selectedModuleIdsSharedFlow, + project.PackageSearchProjectService.modulesByIdentity + ) { selectedModules, modulesByIdentity -> + modulesByIdentity.filterKeys { it in selectedModules }.values.toList() + } + .shareIn(viewModelScope, SharingStarted.Lazily) private val searchQueryMutableStateFlow = MutableStateFlow("") val searchQueryStateFlow = searchQueryMutableStateFlow.asStateFlow() + init { + searchQueryStateFlow + .filter { it.isNotEmpty() } + .onEach { logSearchRequest(it) } + .launchIn(viewModelScope) + + selectedModulesFlow + .onEach { logTargetModuleSelected(it) } + .launchIn(viewModelScope) + } + private val headerCollapsedStatesFlow: MutableStateFlow> = MutableStateFlow(emptyMap()) @@ -137,6 +168,13 @@ class PackageListViewModel( else -> value } } + }.modifiedBy(selectedModulesFlow) { current: Map, change -> + val changeIdentities = change.map { it.identity } + if (current.keys.any { it.moduleIdentity !in changeIdentities }) { + emptyMap() + } else { + current + } } .stateIn(viewModelScope, SharingStarted.Eagerly, emptyMap()) @@ -244,6 +282,13 @@ class PackageListViewModel( headerId to search } + fun clearSearchQuery() { + viewModelScope.launch { + searchQueryMutableStateFlow.emit("") + logSearchQueryClear() + } + } + fun setSearchQuery(searchQuery: String) { viewModelScope.launch { searchQueryMutableStateFlow.emit(searchQuery) } } @@ -264,10 +309,11 @@ class PackageListViewModel( viewModelScope.launch { when (event) { is PackageListItemEvent.EditPackageEvent -> handle(event) - is PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick -> logTODO() - is PackageListItemEvent.InfoPanelEvent.OnHeaderVariantsClick -> logTODO() + is PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick -> handle(event) + is PackageListItemEvent.InfoPanelEvent.OnHeaderVariantsClick -> handle(event) is PackageListItemEvent.InfoPanelEvent.OnPackageSelected -> handle(event) is PackageListItemEvent.InfoPanelEvent.OnPackageDoubleClick -> handle(event) + is PackageListItemEvent.InfoPanelEvent.OnSelectedPackageClick -> handle(event) is PackageListItemEvent.OnPackageAction.GoToSource -> handle(event) is PackageListItemEvent.OnPackageAction.Install.Base -> handle(event) is PackageListItemEvent.OnPackageAction.Install.WithVariant -> handle(event) @@ -279,16 +325,29 @@ class PackageListViewModel( } } + private fun handle(event: PackageListItemEvent.InfoPanelEvent.OnHeaderVariantsClick) { + logTODO() + logHeaderVariantsClick() + } + private suspend fun handle(event: PackageListItemEvent.InfoPanelEvent.OnPackageDoubleClick) { project.service().isInfoPanelOpen.emit(true) + logInfoPanelOpened() + } + + private fun handle(event: PackageListItemEvent.InfoPanelEvent.OnSelectedPackageClick) { + if (event.eventId is PackageListItem.Package.Id) handle( + PackageListItemEvent.InfoPanelEvent.OnPackageSelected(event.eventId) + ) } private fun handle(event: PackageListItemEvent.InfoPanelEvent.OnPackageSelected) { val infoPanelViewModel = project.service() + logPackageSelected(event.eventId is PackageListItem.Package.Declared.Id) when (event.eventId) { is PackageListItem.Package.Remote.Base.Id -> { val headerId = PackageListItem.Header.Id.Remote.Base(event.eventId.moduleIdentity) - val search = searchResultMapFlow.value.getValue(headerId) as? Search.Results.Base + val search = searchResultMapFlow.value[headerId] as? Search.Results.Base ?: return infoPanelViewModel.setPackage( module = event.eventId.getModule() as? PackageSearchModule.Base ?: return, @@ -302,7 +361,7 @@ class PackageListViewModel( event.eventId.moduleIdentity, event.eventId.headerId.compatibleVariantNames ) - val search = searchResultMapFlow.value.getValue(headerId) as? Search.Results.WithVariants + val search = searchResultMapFlow.value[headerId] as? Search.Results.WithVariants ?: return infoPanelViewModel.setPackage( module = event.eventId.getModule() as? PackageSearchModule.WithVariants ?: return, @@ -325,7 +384,8 @@ class PackageListViewModel( val module = event.eventId .getModule() as? PackageSearchModule.WithVariants ?: return - val variant = module.variants.getValue(event.eventId.variantName) + val variant = module.variants[event.eventId.variantName] + ?: return val declaredPackage = variant.declaredDependencies .firstOrNull { it.id == event.eventId.packageId } ?: return @@ -336,7 +396,7 @@ class PackageListViewModel( private suspend fun handle(actionType: PackageListItemEvent.OnPackageAction.Update) { packagesLoadingMutableStateFlow.update { it + actionType.eventId } - val (editor, manager, dependency) = + val (module, editor, manager, dependency) = actionType.eventId.getDependencyManagers() ?: return val newVersion = when { project.PackageSearchProjectService.stableOnlyStateFlow.value -> @@ -345,6 +405,12 @@ class PackageListViewModel( else -> dependency.remoteInfo?.versions?.latest?.normalized?.versionName } ?: return + logPackageVersionChanged( + packageIdentifier = dependency.id, + packageFromVersion = dependency.declaredVersion?.versionName, + packageTargetVersion = newVersion, + targetModule = module + ) editor.editModule { manager.updateDependency(dependency, newVersion, dependency.declaredScope) } @@ -359,6 +425,11 @@ class PackageListViewModel( val declaredPackage = module.declaredDependencies .firstOrNull { it.id == actionType.eventId.packageId } ?: return@editModule + logPackageRemoved( + packageIdentifier = actionType.eventId.packageId, + packageVersion = declaredPackage.declaredVersion?.versionName, + targetModule = module + ) module.removeDependency(declaredPackage) } @@ -366,11 +437,16 @@ class PackageListViewModel( val eventId = actionType .eventId as? PackageListItem.Package.Declared.Id.WithVariant ?: return@editModule - val variant = module.variants - .getValue(eventId.variantName) + val variant = module.variants[eventId.variantName] + ?: return@editModule val declaredPackage = variant.declaredDependencies .firstOrNull { it.id == eventId.packageId } ?: return@editModule + logPackageRemoved( + packageIdentifier = actionType.eventId.packageId, + packageVersion = declaredPackage.declaredVersion?.versionName, + targetModule = module + ) variant.removeDependency(declaredPackage) } } @@ -382,13 +458,15 @@ class PackageListViewModel( val module = actionType.eventId .getModule() as? PackageSearchModule.WithVariants ?: return - val variant = module.variants.getValue(actionType.selectedVariantName) + val variant = module.variants[actionType.selectedVariantName] + ?: return val search = searchResultMapFlow .value[actionType.headerId] as? Search.Results.WithVariants ?: return val apiPackage = search.packages .firstOrNull { it.id == actionType.eventId.packageId } ?: return + logPackageInstalled(apiPackage.id, module) installDependency( manager = variant, updater = module, @@ -406,6 +484,7 @@ class PackageListViewModel( val apiPackage = search.packages .firstOrNull { it.id == actionType.eventId.packageId } ?: return + logPackageInstalled(apiPackage.id, module) installDependency( manager = module, updater = module, @@ -443,12 +522,14 @@ class PackageListViewModel( val eventId = actionType .eventId as? PackageListItem.Package.Declared.Id.WithVariant ?: return - val variant = module.variants.getValue(eventId.variantName) + val variant = module.variants[eventId.variantName] + ?: return variant.declaredDependencies.firstOrNull { it.id == eventId.packageId } } null -> return } ?: return + logGoToSource(module, dependency.id) val buildFile = module.buildFilePath ?.let { LocalFileSystem.getInstance().findFileByNioFile(it) } ?: return @@ -474,22 +555,44 @@ class PackageListViewModel( private suspend fun handle(event: PackageListItemEvent.EditPackageEvent) { packagesLoadingMutableStateFlow.update { it + event.eventId } + val (module, editor, manager, dependency) = + event.eventId.getDependencyManagers() ?: return runCatching { - val (editor, manager, dependency) = - event.eventId.getDependencyManagers() ?: return editor.editModule { when (event) { - is PackageListItemEvent.EditPackageEvent.SetPackageScope -> + is PackageListItemEvent.EditPackageEvent.SetPackageScope -> { + viewModelScope.launch { + logPackageScopeChanged(dependency.id, dependency.declaredScope, event.scope, module) + } manager.updateDependency( declaredPackage = dependency, newVersion = dependency.declaredVersion?.versionName, newScope = event.scope ) + } - is PackageListItemEvent.EditPackageEvent.SetPackageVersion -> - manager.updateDependency(dependency, event.version, dependency.declaredScope) + is PackageListItemEvent.EditPackageEvent.SetPackageVersion -> { + viewModelScope.launch { + logPackageVersionChanged( + packageIdentifier = dependency.id, + packageFromVersion = dependency.declaredVersion?.versionName, + packageTargetVersion = event.version, + targetModule = module + ) + } + manager.updateDependency( + declaredPackage = dependency, + newVersion = event.version, + newScope = dependency.declaredScope + ) + } - is PackageListItemEvent.EditPackageEvent.SetVariant -> logTODO() + is PackageListItemEvent.EditPackageEvent.SetVariant -> { + viewModelScope.launch { + logPackageVariantChanged(dependency.id, module) + } + logTODO() + } } } } @@ -498,15 +601,57 @@ class PackageListViewModel( } } + private suspend fun handle(event: PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick) { + logHeaderAttributesClick(isSearchHeader = event.eventId is PackageListItem.Header.Id.Remote) + val infoPanelViewModel = project.service() + + val module = event.eventId.getModule() as? PackageSearchModule.WithVariants ?: return + + when (event) { + is PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick.DeclaredHeaderAttributesClick -> { + val attributes = module.variants[event.variantName]?.attributes ?: return + infoPanelViewModel.setDeclaredHeaderAttributes(event.variantName, attributes = attributes) + } + + is PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick.SearchHeaderAttributesClick -> { + val attributes = module + .variants + .map { it.value.attributes } + .flatten() + .filter { it.value in event.attributesNames } + .distinct() + + val variants = module.variants.values.map { it.name } - module.mainVariantName + + infoPanelViewModel.setSearchHeaderAttributes( + defaultVariant = module.mainVariantName, + additionalVariants = variants, + attributes = attributes + ) + } + } + + + project.service().isInfoPanelOpen.let { openStateFlow -> + if (!openStateFlow.value) { + openStateFlow.emit(true) + } + } + + } + + private suspend fun handle(event: PackageListItemEvent.EditPackageEvent.SetVariant) { val module = event.eventId .getModule() as? PackageSearchModule.WithVariants ?: return - val variant = module.variants.getValue(event.eventId.variantName) + val variant = module.variants[event.eventId.variantName] + ?: return val declaredPackage = variant.declaredDependencies .firstOrNull { it.id == event.eventId.packageId } ?: return - val newVariant = module.variants.getValue(event.selectedVariantName) + val newVariant = module.variants[event.selectedVariantName] + ?: return module.editModule { variant.removeDependency(declaredPackage) // newVariant.addDependency( @@ -519,6 +664,7 @@ class PackageListViewModel( private suspend fun handle(event: PackageListItemEvent.UpdateAllPackages) { headerLoadingStatesFlow.update { it + event.eventId } + logUpgradeAll() val onlyStable = project.PackageSearchProjectService.stableOnlyStateFlow.value when (val module = event.eventId.getModule()) { is PackageSearchModule.Base -> { @@ -602,8 +748,10 @@ class PackageListViewModel( val withVariants = modulesById[moduleIdentity] as? PackageSearchModule.WithVariants ?: return null - val variant = withVariants.variants.getValue(variantName) + val variant = withVariants.variants[variantName] + ?: return null PackageSearchDependencyHandlers( + module = withVariants, modifier = withVariants, manager = variant, declaredPackage = variant.declaredDependencies @@ -614,4 +762,7 @@ class PackageListViewModel( } } + override fun dispose() { + viewModelScope.cancel() + } } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/Utils.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/Utils.kt index 15378b24..a381ec39 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/Utils.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/Utils.kt @@ -57,6 +57,7 @@ internal fun buildPackageList( ).apply(block).build() internal data class PackageSearchDependencyHandlers( + val module: PackageSearchModule, val modifier: PackageSearchModuleEditor, val manager: PackageSearchDependencyManager, val declaredPackage: PackageSearchDeclaredPackage, @@ -65,8 +66,12 @@ internal data class PackageSearchDependencyHandlers( internal fun PackageSearchDependencyHandlers( module: PackageSearchModule.Base, declaredPackage: PackageSearchDeclaredPackage, -) = - PackageSearchDependencyHandlers(module, module, declaredPackage) +) = PackageSearchDependencyHandlers( + module = module, + modifier = module, + manager = module, + declaredPackage = declaredPackage +) internal fun combineListChanges( modulesFlow: Flow>, diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/tree/TreeViewModel.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/tree/TreeViewModel.kt index b1815893..f162a8d3 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/tree/TreeViewModel.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/tree/TreeViewModel.kt @@ -1,6 +1,7 @@ package com.jetbrains.packagesearch.plugin.ui.model.tree import androidx.compose.foundation.lazy.LazyListState +import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service.Level import com.intellij.openapi.project.Project @@ -8,6 +9,8 @@ import com.jetbrains.packagesearch.plugin.core.utils.IntelliJApplication import com.jetbrains.packagesearch.plugin.utils.PackageSearchApplicationCachesService import com.jetbrains.packagesearch.plugin.utils.PackageSearchProjectService import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -18,10 +21,9 @@ import org.jetbrains.jewel.foundation.lazy.tree.TreeState import org.jetbrains.jewel.foundation.lazy.tree.emptyTree @Service(Level.PROJECT) -internal class TreeViewModel( - project: Project, - viewModelScope: CoroutineScope, -) { +internal class TreeViewModel(project: Project) : Disposable { + + private val viewModelScope: CoroutineScope = CoroutineScope(SupervisorJob()) val tree: StateFlow> = combine( project.PackageSearchProjectService.modulesStateFlow, @@ -31,12 +33,11 @@ internal class TreeViewModel( } .stateIn(viewModelScope, SharingStarted.Lazily, emptyTree()) - val treeState = TreeState(SelectableLazyListState(LazyListState())) + internal val lazyListState = LazyListState() + internal val treeState = TreeState(SelectableLazyListState(lazyListState)) val isOnline - get() = IntelliJApplication.PackageSearchApplicationCachesService - .apiPackageCache - .isOnlineFlow + get() = IntelliJApplication.PackageSearchApplicationCachesService.isOnlineFlow fun expandAll() { treeState.openNodes = tree.value.walkBreadthFirst().map { it.id }.toSet() @@ -46,5 +47,9 @@ internal class TreeViewModel( treeState.openNodes = emptySet() } + override fun dispose() { + viewModelScope.cancel() + } + } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchCentralPanel.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchCentralPanel.kt index fbd9ee59..7d11c0b0 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchCentralPanel.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchCentralPanel.kt @@ -2,20 +2,24 @@ package com.jetbrains.packagesearch.plugin.ui.panels.packages import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListViewModel import com.jetbrains.packagesearch.plugin.ui.viewModel import org.jetbrains.jewel.ui.Orientation import org.jetbrains.jewel.ui.component.Divider import org.jetbrains.jewel.ui.component.IndeterminateHorizontalProgressBar +import org.jetbrains.jewel.ui.component.VerticalScrollbar @Composable fun PackageSearchCentralPanel( - modifier: Modifier, + modifier: Modifier = Modifier, onLinkClick: (String) -> Unit, ) = Column(modifier) { val viewModel: PackageListViewModel = viewModel() @@ -25,6 +29,7 @@ fun PackageSearchCentralPanel( onlineSearchEnabled = isOnlineSearchEnabled, searchQuery = searchQuery, onSearchQueryChange = { viewModel.setSearchQuery(it) }, + onSearchQueryClear = { viewModel.clearSearchQuery() } ) Divider(Orientation.Horizontal) val packagesList by viewModel.packageListItemsFlow.collectAsState() @@ -39,6 +44,10 @@ fun PackageSearchCentralPanel( selectableLazyListState = viewModel.selectableLazyListState, onPackageEvent = viewModel::onPackageListItemEvent, ) + VerticalScrollbar( + adapter = rememberScrollbarAdapter(scrollState = viewModel.selectableLazyListState.lazyListState), + modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd), + ) } } val isLoading by viewModel.isLoadingFlow.collectAsState() diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchPackageList.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchPackageList.kt index 7660a4d3..2a745c06 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchPackageList.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchPackageList.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.onClick -import androidx.compose.material.Divider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -36,7 +35,7 @@ import com.jetbrains.packagesearch.plugin.ui.LearnMoreLink import com.jetbrains.packagesearch.plugin.ui.PackageSearchMetrics import com.jetbrains.packagesearch.plugin.ui.bridge.LabelInfo import com.jetbrains.packagesearch.plugin.ui.bridge.PackageActionPopup -import com.jetbrains.packagesearch.plugin.ui.bridge.TextSelectionDropdown +import com.jetbrains.packagesearch.plugin.ui.bridge.PackageSearchDropdownLink import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItemEvent import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItemEvent.EditPackageEvent.SetPackageScope @@ -46,13 +45,15 @@ import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItemE import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItemEvent.OnPackageAction.GoToSource import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItemEvent.OnPackageAction.Install import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItemEvent.OnPackageAction.Remove -import com.jetbrains.packagesearch.plugin.ui.panels.packages.items.PackageListItem +import com.jetbrains.packagesearch.plugin.ui.panels.packages.items.PackageListHeader import org.jetbrains.jewel.foundation.lazy.SelectableLazyColumn import org.jetbrains.jewel.foundation.lazy.SelectableLazyItemScope import org.jetbrains.jewel.foundation.lazy.SelectableLazyListState import org.jetbrains.jewel.foundation.lazy.SelectionMode import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.Orientation import org.jetbrains.jewel.ui.component.CircularProgressIndicator +import org.jetbrains.jewel.ui.component.Divider import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.Link import org.jetbrains.jewel.ui.component.Text @@ -60,6 +61,7 @@ import org.jetbrains.jewel.ui.component.styling.LocalLazyTreeStyle @Composable fun PackageSearchPackageList( + modifier: Modifier = Modifier, packagesList: List, isCompact: Boolean, selectableLazyListState: SelectableLazyListState, @@ -67,6 +69,7 @@ fun PackageSearchPackageList( ) { var openPopupId by remember { mutableStateOf(null) } SelectableLazyColumn( + modifier = modifier, selectionMode = SelectionMode.Single, state = selectableLazyListState, onSelectedIndexesChanged = { @@ -80,12 +83,17 @@ fun PackageSearchPackageList( ) { packagesList.forEachIndexed { index, item -> when (item) { - is PackageListItem.Header -> stickyHeader(item.id, "header") { - PackageListItem(item, onPackageEvent) + is PackageListItem.Header -> stickyHeader(key = item.id, contentType = "header") { + PackageListHeader( + additionalContentModifier = Modifier.padding(end = PackageSearchMetrics.scrollbarWidth), + content = item, + onEvent = onPackageEvent + ) } - is PackageListItem.Package -> item(item.id, contentType = item.contentType()) { + is PackageListItem.Package -> item(key = item.id, contentType = item.contentType()) { PackageListItem( + modifier = Modifier.padding(end = PackageSearchMetrics.scrollbarWidth), content = item, packagesList = packagesList, index = index, @@ -102,7 +110,8 @@ fun PackageSearchPackageList( } @Composable -private fun SelectableLazyItemScope.PackageListItem( +internal fun SelectableLazyItemScope.PackageListItem( + modifier: Modifier = Modifier, content: PackageListItem.Package, packagesList: List, index: Int, @@ -117,12 +126,17 @@ private fun SelectableLazyItemScope.PackageListItem( isLastItem = packagesList.getOrNull(index + 1) !is PackageListItem.Package ) Box( - modifier = Modifier + modifier = modifier .padding(itemPaddings) .onClick( interactionSource = remember { MutableInteractionSource() }, onDoubleClick = { onPackageListItemEvent(OnPackageDoubleClick(content.id)) }, - onClick = { } + onClick = { + //this event should be handled when you click on a selected package to refresh side panel content + if (isSelected ) onPackageListItemEvent( + PackageListItemEvent.InfoPanelEvent.OnSelectedPackageClick(content.id) + ) + } ), ) { Row( @@ -165,7 +179,6 @@ private fun ScopeAndVersionDropdowns( Row() { Box(modifier = Modifier.widthIn(max = 180.dp)) { ScopeSelectionDropdown( - modifier = Modifier.align(Alignment.CenterEnd), declaredScope = item.selectedScope, allowMissingScope = item.allowMissingScope, availableScopes = item.availableScopes, @@ -174,9 +187,8 @@ private fun ScopeAndVersionDropdowns( ) } - Box(modifier = Modifier.width(180.dp)) { + Box(modifier = Modifier.width(180.dp), contentAlignment = Alignment.CenterEnd) { VersionSelectionDropdown( - modifier = Modifier.align(Alignment.CenterEnd), declaredVersion = item.declaredVersion, availableVersions = item.availableVersions, latestVersion = item.latestVersion, @@ -313,7 +325,7 @@ internal fun RemotePackageWithVariantsActionPopup( } if (!isInstalledInPrimaryVariant) { passiveItem { - Divider(modifier = Modifier.padding(vertical = 4.dp)) + Divider(Orientation.Horizontal,modifier = Modifier.padding(vertical = 4.dp)) } selectableItem( selected = false, @@ -325,7 +337,7 @@ internal fun RemotePackageWithVariantsActionPopup( if (additionalVariants.isNotEmpty()) { passiveItem { - Divider(modifier = Modifier.padding(vertical = 4.dp)) + Divider(Orientation.Horizontal,modifier = Modifier.padding(vertical = 4.dp)) } additionalVariants.forEach { selectableItem( @@ -369,7 +381,7 @@ internal fun DeclaredPackageActionPopup( } } passiveItem { - Divider(modifier = Modifier.padding(vertical = 4.dp)) + Divider(Orientation.Horizontal,modifier = Modifier.padding(vertical = 4.dp)) } selectableItem( selected = false, @@ -431,7 +443,7 @@ fun VersionSelectionDropdown( } } } - TextSelectionDropdown( + PackageSearchDropdownLink( modifier = modifier, menuModifier = menuModifier, items = availableVersions, @@ -451,7 +463,7 @@ fun ScopeSelectionDropdown( enabled: Boolean, onScopeChanged: (String?) -> Unit, ) { - TextSelectionDropdown( + PackageSearchDropdownLink( modifier = modifier, menuModifier = menuModifier, items = buildList { diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchSearchBar.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchSearchBar.kt index 5dd1f894..8de444f4 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchSearchBar.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchSearchBar.kt @@ -6,31 +6,24 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.intellij.icons.AllIcons import com.jetbrains.packagesearch.plugin.PackageSearchBundle.message import com.jetbrains.packagesearch.plugin.ui.PackageSearchMetrics -import org.jetbrains.jewel.foundation.GlobalColors -import org.jetbrains.jewel.foundation.LocalGlobalColors -import org.jetbrains.jewel.foundation.OutlineColors import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.IconButton import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.TextField -import org.jetbrains.jewel.ui.component.styling.LocalDefaultTabStyle import org.jetbrains.jewel.ui.component.styling.LocalTextFieldStyle -import org.jetbrains.jewel.ui.component.styling.TabMetrics -import org.jetbrains.jewel.ui.component.styling.TabStyle @Composable fun PackageSearchSearchBar( onlineSearchEnabled: Boolean, searchQuery: String, onSearchQueryChange: (String) -> Unit, + onSearchQueryClear: () -> Unit, ) { Row( modifier = Modifier @@ -66,7 +59,7 @@ fun PackageSearchSearchBar( trailingIcon = { Crossfade(searchQuery.isEmpty()) { if (it) return@Crossfade - IconButton(onClick = { onSearchQueryChange("") }) { + IconButton(onClick = { onSearchQueryClear() }) { Icon( resource = "actions/close.svg", contentDescription = null, @@ -78,43 +71,4 @@ fun PackageSearchSearchBar( ) } - -} - -@Composable -internal fun packageSearchTabStyle(): TabStyle { - val current = LocalDefaultTabStyle.current - return TabStyle( - colors = current.colors, - metrics = TabMetrics( - underlineThickness = current.metrics.underlineThickness, - tabPadding = current.metrics.tabPadding, - tabHeight = PackageSearchMetrics.searchBarHeight, - closeContentGap = current.metrics.closeContentGap, - ), - icons = current.icons, - contentAlpha = current.contentAlpha - ) -} - - -@Composable -fun packageSearchGlobalColors(): GlobalColors { - val colors = LocalGlobalColors.current - - return remember(colors) { - GlobalColors( - borders = colors.borders, - outlines = OutlineColors( - focused = Color.Transparent, - focusedWarning = colors.outlines.focusedWarning, - focusedError = colors.outlines.focusedError, - warning = colors.outlines.warning, - error = colors.outlines.error, - ), - infoContent = colors.infoContent, - paneBackground = colors.paneBackground, - ) - } } - diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/items/PackageGroupHeader.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/items/PackageGroupHeader.kt index 86175c3b..3151cb0f 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/items/PackageGroupHeader.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/items/PackageGroupHeader.kt @@ -10,6 +10,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.onClick import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -19,30 +23,30 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.intellij.icons.AllIcons import com.jetbrains.packagesearch.plugin.PackageSearchBundle.message +import com.jetbrains.packagesearch.plugin.ui.PackageSearchColors import com.jetbrains.packagesearch.plugin.ui.bridge.LabelInfo -import com.jetbrains.packagesearch.plugin.ui.bridge.pickComposeColorFromLaf import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem.Header.State import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItemEvent import java.awt.Cursor +import org.jetbrains.jewel.foundation.modifier.onHover import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.component.CircularProgressIndicator import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.Link import org.jetbrains.jewel.ui.component.Text +import org.jetbrains.jewel.ui.theme.linkStyle @Composable -fun PackageListItem( +fun PackageListHeader( + additionalContentModifier: Modifier = Modifier, content: PackageListItem.Header, onEvent: (PackageListItemEvent) -> Unit, ) { - // TODO check if needed - val backgroundColor = - if (JewelTheme.isDark) { - pickComposeColorFromLaf("ToolWindow.HeaderTab.selectedInactiveBackground") - } else { - pickComposeColorFromLaf("Tree.selectionInactiveBackground") - } + val isDarkTheme = JewelTheme.isDark + val backgroundColor = remember(isDarkTheme) { + PackageSearchColors.Backgrounds.packageItemHeader() + } Row( modifier = Modifier @@ -54,7 +58,7 @@ fun PackageListItem( verticalAlignment = Alignment.CenterVertically, ) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Box( + Row( modifier = Modifier.onClick(enabled = content.state != State.LOADING) { onEvent( PackageListItemEvent.SetHeaderState( @@ -65,6 +69,8 @@ fun PackageListItem( ) ) } + .pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR))), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { when (content.state) { State.OPEN -> Icon( @@ -83,41 +89,64 @@ fun PackageListItem( State.LOADING -> CircularProgressIndicator() } + + Text( + fontWeight = FontWeight.ExtraBold, + text = content.title, + maxLines = 1 + ) } - Text( - fontWeight = FontWeight(600), - text = content.title, - maxLines = 1 - ) - content.count?.let { LabelInfo(text = it.toString()) } - if (content.attriutes.isNotEmpty()) { + if (content.attributes.isNotEmpty()) { + var attributeTextColor by remember { mutableStateOf(Color.Unspecified) } + val linkTextColor = JewelTheme.linkStyle.colors.content Box( modifier = Modifier .onClick { - onEvent(PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick(content.id)) + val event = + when (content.id) { + is PackageListItem.Header.Id.Declared.Base -> return@onClick + is PackageListItem.Header.Id.Remote -> PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick.SearchHeaderAttributesClick( + eventId = content.id, + attributesNames = content.attributes + ) + is PackageListItem.Header.Id.Declared.WithVariant -> PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick.DeclaredHeaderAttributesClick( + eventId = content.id, + variantName = content.title, + ) + } + onEvent(event) + } + .onHover { + attributeTextColor = if (it) linkTextColor else Color.Unspecified } .pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR))), contentAlignment = Alignment.Center, ) { - LabelInfo( - text = content.attriutes.joinToString(" "), + Text( + text = content.attributes.joinToString(" "), + color = attributeTextColor, maxLines = 1 ) } } } if (content.additionalContent != null) { - when (content.additionalContent) { - is PackageListItem.Header.AdditionalContent.VariantsText -> - LabelInfo( - text = content.additionalContent.text, - maxLines = 1 - ) + Box( + modifier = additionalContentModifier, + ) { - is PackageListItem.Header.AdditionalContent.UpdatesAvailableCount -> - UpdateAllLink(content.additionalContent, content, onEvent) + when (content.additionalContent) { + is PackageListItem.Header.AdditionalContent.VariantsText -> + LabelInfo( + text = content.additionalContent.text, + maxLines = 1 + ) + + is PackageListItem.Header.AdditionalContent.UpdatesAvailableCount -> + UpdateAllLink(content.additionalContent, content, onEvent) - PackageListItem.Header.AdditionalContent.Loading -> CircularProgressIndicator() + PackageListItem.Header.AdditionalContent.Loading -> CircularProgressIndicator() + } } } } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/HeaderAttributesTab.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/HeaderAttributesTab.kt new file mode 100644 index 00000000..2715203a --- /dev/null +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/HeaderAttributesTab.kt @@ -0,0 +1,286 @@ +package com.jetbrains.packagesearch.plugin.ui.panels.side + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.jetbrains.packagesearch.plugin.PackageSearchBundle +import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModuleVariant +import com.jetbrains.packagesearch.plugin.ui.bridge.AttributeBadge +import com.jetbrains.packagesearch.plugin.ui.bridge.LabelInfo +import com.jetbrains.packagesearch.plugin.ui.model.infopanel.InfoPanelContent +import kotlin.math.roundToInt +import kotlin.random.Random +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.jetbrains.jewel.ui.component.Text + +@Composable +fun HeaderAttributesTab( + content: InfoPanelContent.Attributes, + scrollState: ScrollState, +) { + HeaderAttributesTabImpl( + content = content, + scrollState = scrollState, + contentTitle = PackageSearchBundle.message("packagesearch.ui.toolwindow.sidepanel.searchResultSupport"), + attributeTypeName = PackageSearchBundle.message("packagesearch.ui.toolwindow.sidepanel.platformsList"), + sourceSetString = PackageSearchBundle.message("packagesearch.ui.toolwindow.sidepanel.searchResultSupport.sourceSets"), + ) +} + +@Composable +private fun HeaderAttributesTabImpl( + content: InfoPanelContent.Attributes, + contentTitle: String, + scrollState: ScrollState, + attributeTypeName: String, + sourceSetString: String, +) { + val scope = rememberCoroutineScope() + val attributes = content.attributes + // Global positions of attributes will be used to store the y offset used on scrollToItem + val attributeGlobalPositionMap = remember { mutableMapOf() } + + Column(verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(12.dp)) { + if (content is InfoPanelContent.Attributes.FromSearch) { + Text( + text = contentTitle, + fontSize = 14.sp, + fontWeight = FontWeight.ExtraBold, + modifier = Modifier.padding(top = 4.dp) + ) + } + + + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + attributes.forEachIndexed { index, attribute -> + AttributeBadge(text = attribute.value) { + scope.scrollToAttribute(scrollState, attributeGlobalPositionMap, index) + } + } + } + + if (content is InfoPanelContent.Attributes.FromSearch) { + SourceSetsList(content, sourceSetString) + } + + AttributeItems(attributeTypeName = attributeTypeName, attributes, attributeGlobalPositionMap) + } +} + + +@Composable +private fun ColumnScope.SourceSetsList( + content: InfoPanelContent.Attributes.FromSearch, + title: String, +) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = title, + fontSize = 14.sp, + fontWeight = FontWeight.ExtraBold, + modifier = Modifier.padding(top = 4.dp) + ) + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Text(text = content.defaultSourceSet) + LabelInfo(PackageSearchBundle.message("packagesearch.ui.toolwindow.variant.default.text")) + } + Text(text = content.additionalSourceSets.joinToString(", ")) + + } +} + +@Composable +private fun AttributeItems( + attributeTypeName: String, + attributesName: List, + attributeGlobalPosition: MutableMap, +) { + Text( + text = attributeTypeName, + fontSize = 14.sp, + fontWeight = FontWeight.ExtraBold, + modifier = Modifier.padding(top = 4.dp) + ) + + attributesName.forEachIndexed { index, attribute -> + AttributeItem( + modifier = Modifier.onGloballyPositioned { + attributeGlobalPosition[index] = it.positionInParent().y.roundToInt() + }, + attributeName = attribute.value, + nestedAttributesName = attribute.flatten() + ) + } +} + +private fun PackageSearchModuleVariant.Attribute.flatten(): List = + when (this) { + is PackageSearchModuleVariant.Attribute.NestedAttribute -> children.flatMap { it.flatten() } + is PackageSearchModuleVariant.Attribute.StringAttribute -> listOf(value) + } + + +private fun CoroutineScope.scrollToAttribute( + scrollState: ScrollState, + attributeGlobalPosition: MutableMap, + index: Int, +) { + launch { + scrollState.animateScrollTo( + value = attributeGlobalPosition[index] ?: 0, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessVeryLow + ) + ) + } +} + +@Composable +fun AttributeItem(modifier: Modifier = Modifier, attributeName: String, nestedAttributesName: List) { + + Row(modifier = modifier, verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Row( + modifier = Modifier.width(160.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = attributeName) + LabelInfo(nestedAttributesName.size.toString()) + } + + Text(text = nestedAttributesName.joinToString("\n")) + } +} + + +//@Preview +//@Composable +//private fun HeaderAttributesPreviewTab() { +// val activeTabMock = InfoPanelContent.Attributes.FromVariant( +// tabTitle = "FromVariant", +// variantName = "jvm", +// attributes = generateAttributesMock() +// ) +// val scrollStateMock = ScrollState(0) +// Column(Modifier.padding(8.dp)) { +// IntUiTheme(true) { +// Column(Modifier.background(LocalGlobalColors.current.paneBackground).padding(16.dp)) { +// HeaderAttributesTabImpl( +// content = activeTabMock, +// scrollState = scrollStateMock, +// contentTitle = "FromSearch results that support:", +// attributeTypeName = "Platforms:", +// sourceSetString = "Source Sets:" +// ) +// +// } +// +// Divider(orientation = Orientation.Horizontal, modifier = Modifier.padding(vertical = 16.dp)) +// +// } +// +// +// IntUiTheme(false) { +// Column(Modifier.background(LocalGlobalColors.current.paneBackground).padding(16.dp)) { +// HeaderAttributesTabImpl( +// content = activeTabMock, +// scrollState = scrollStateMock, +// contentTitle = "FromSearch results that support:", +// attributeTypeName = "Platforms:", +// sourceSetString = "Source Sets:" +// ) +// +// } +// } +// } +//} + +private fun generateAttributesMock(): List { + val attributes = mutableListOf() + + platformListMock.take(Random.nextInt(2, 10)).forEach { + attributes.add( + if (Random.nextBoolean()) { + PackageSearchModuleVariant.Attribute.NestedAttribute( + it, + platformListMock.take(Random.nextInt(2, 10)).map { + PackageSearchModuleVariant.Attribute.StringAttribute(it) + } + ) + + + } else { + + PackageSearchModuleVariant.Attribute.StringAttribute(it) + } + ) + } + + return attributes + +} + +internal val platformListMock get() = buildList { + add("Android") + add("android") + add("Apple") + add("iOS") + add("iosX64") + add("iosArm64") + add("iosSimulatorArm64") + add("macOS") + add("macosArm64") + add("macosX64") + + add("watchOS") + add("watchosArm32") + add("watchosArm64") + add("watchosX64") + add("watchosSimulatorArm64") + + add("tvOS") + add("tvosX64") + add("tvosArm64") + add("tvosSimulatorArm64") + + + add("Java") + add("jvm") + + add("JavaScript") + add("jsLegacy") + add("jslr") + + add("Linux") + add("LinuxMipsel32") + add("LinuxArm64") + add("LinuxArm32Hfp") + add("LinuxX64") + + add("Windows") + add("WindowsX64") + add("WindowsX86") + + add("WebAssembly") + add("wasm") + add("wasm32") + +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageOverviewInfo.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageOverviewTab.kt similarity index 86% rename from plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageOverviewInfo.kt rename to plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageOverviewTab.kt index 25785349..0627a8eb 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageOverviewInfo.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageOverviewTab.kt @@ -18,9 +18,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.jetbrains.packagesearch.plugin.PackageSearchBundle import com.jetbrains.packagesearch.plugin.PackageSearchBundle.message import com.jetbrains.packagesearch.plugin.core.data.IconProvider +import com.jetbrains.packagesearch.plugin.fus.FUSGroupIds +import com.jetbrains.packagesearch.plugin.fus.logDetailsLinkClick import com.jetbrains.packagesearch.plugin.ui.bridge.LabelInfo import com.jetbrains.packagesearch.plugin.ui.model.infopanel.InfoPanelContent import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItemEvent @@ -48,14 +49,11 @@ internal fun PackageOverviewTab( ) { Column( verticalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize().padding(start = 4.dp) ) { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.Top - ) { - InfoPanelPackageTitle(modifier = Modifier.weight(1f), content.title, content.subtitle) - InfoPanelPackageActions(content, onPackageEvent) + Row(verticalAlignment = Alignment.Top) { + PackageTitle(modifier = Modifier.weight(1f), content.title, content.subtitle) + Actions(content, onPackageEvent) } Column( modifier = Modifier.fillMaxWidth().padding(12.dp), @@ -63,7 +61,7 @@ internal fun PackageOverviewTab( ) { if (content is InfoPanelContent.PackageInfo.Declared) { Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.Top ) { LabelInfo( @@ -80,7 +78,7 @@ internal fun PackageOverviewTab( } } Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.Top ) { LabelInfo( @@ -98,18 +96,18 @@ internal fun PackageOverviewTab( } } - content.type?.let { PackageType(it, content.icon) } + content.type?.let { PackageType(it) } if (content.repositories.isNotEmpty()) { - InfoPanelPackageDetailLine( + DetailLabel( name = message("packagesearch.ui.toolwindow.packages.details.info.repositories"), value = content.repositories.map { it.name }.joinToString(", ") ) } if (content.licenses.isNotEmpty()) { - InfoPanelPackageLinks(content.licenses, onLinkClick) + PackageLinks(content.licenses, onLinkClick) } if (content.authors.isNotEmpty()) { - InfoPanelPackageDetailLine( + DetailLabel( name = message("packagesearch.ui.toolwindow.packages.details.info.authors"), value = content.authors.joinToString(", ") ) @@ -118,25 +116,30 @@ internal fun PackageOverviewTab( Text( modifier = Modifier.fillMaxWidth(), text = description.trimStart(), - textAlign = TextAlign.Justify, + textAlign = TextAlign.Start, overflow = TextOverflow.Ellipsis, ) } content.scm?.let { - InfoPanelPackageScmLinks(it, onLinkClick) + ScmLinks(it, onLinkClick) } + content.readmeUrl?.let { readmeUrl -> ExternalLink( text = message("packagesearch.ui.toolwindow.link.readme.capitalized"), - onClick = { onLinkClick(readmeUrl) }) + onClick = { + onLinkClick(readmeUrl) + logDetailsLinkClick(FUSGroupIds.DetailsLinkTypes.Readme) + } + ) } } } } @Composable -private fun InfoPanelPackageActions( +private fun Actions( tabContent: InfoPanelContent.PackageInfo, onPackageEvent: (PackageListItemEvent) -> Unit, ) { @@ -221,39 +224,41 @@ private fun InfoPanelPackageActions( } @Composable -private fun PackageType(name: String, icon: IconProvider.Icon) { - Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) { +private fun PackageType(type: InfoPanelContent.PackageInfo.Type) { + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically + ) { LabelInfo( modifier = Modifier.defaultMinSize(90.dp), text = message("packagesearch.ui.toolwindow.packages.columns.type") ) - val iconPath = if (JewelTheme.isDark) icon.darkIconPath else icon.lightIconPath + val iconPath = if (JewelTheme.isDark) type.icon.darkIconPath else type.icon.lightIconPath Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { Icon(iconPath, null, IconProvider::class.java) - Text(name) + Text(type.name) } } } @Composable -private fun InfoPanelPackageDetailLine(name: String, value: String) { +private fun DetailLabel(name: String, value: String) { Row( horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.Top, ) { LabelInfo( - modifier = Modifier.defaultMinSize(90.dp), + modifier = Modifier.defaultMinSize(90.dp, 16.dp), text = name ) Text(value) } } - @Composable -private fun InfoPanelPackageScmLinks( +private fun ScmLinks( scm: InfoPanelContent.PackageInfo.Scm, onLinkClick: (String) -> Unit, ) { @@ -263,7 +268,10 @@ private fun InfoPanelPackageScmLinks( ) { ExternalLink( text = message("packagesearch.ui.toolwindow.link.github"), - onClick = { onLinkClick(scm.url) }, + onClick = { + logDetailsLinkClick(FUSGroupIds.DetailsLinkTypes.Scm) + onLinkClick(scm.url) + }, ) if (scm is InfoPanelContent.PackageInfo.Scm.GitHub) { Icon(resource = "icons/Rating.svg", contentDescription = null, IconProvider::class.java) @@ -273,7 +281,7 @@ private fun InfoPanelPackageScmLinks( } @Composable -private fun InfoPanelPackageTitle( +private fun PackageTitle( modifier: Modifier = Modifier, name: String?, id: String, @@ -283,14 +291,14 @@ private fun InfoPanelPackageTitle( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - org.jetbrains.jewel.ui.component.Text(name ?: id, fontWeight = FontWeight.Bold) + Text(name ?: id, fontWeight = FontWeight.Bold) if (name != null) LabelInfo(id) } } } @Composable -private fun InfoPanelPackageLinks( +private fun PackageLinks( licenses: List, onLinkClick: (String) -> Unit, ) { @@ -300,14 +308,17 @@ private fun InfoPanelPackageLinks( ) { LabelInfo( modifier = Modifier.defaultMinSize(90.dp), - text = PackageSearchBundle.message("packagesearch.ui.toolwindow.packages.details.info.licenses") + text = message("packagesearch.ui.toolwindow.packages.details.info.licenses") ) licenses.forEachIndexed { index, license -> when (license.url) { null -> Text(license.name) else -> ExternalLink( text = license.name, - onClick = { onLinkClick(license.url) }, + onClick = { + onLinkClick(license.url) + logDetailsLinkClick(FUSGroupIds.DetailsLinkTypes.License) + }, ) } if (index != licenses.lastIndex) { diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageSearchInfoPanel.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageSearchInfoPanel.kt index dedaf526..8e5296e4 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageSearchInfoPanel.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageSearchInfoPanel.kt @@ -15,57 +15,66 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import com.jetbrains.packagesearch.plugin.PackageSearchBundle +import com.jetbrains.packagesearch.plugin.ui.PackageSearchMetrics import com.jetbrains.packagesearch.plugin.ui.bridge.LabelInfo import com.jetbrains.packagesearch.plugin.ui.model.infopanel.InfoPanelContent import com.jetbrains.packagesearch.plugin.ui.model.infopanel.InfoPanelViewModel import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItemEvent import com.jetbrains.packagesearch.plugin.ui.viewModel +import org.jetbrains.jewel.ui.component.SimpleTabContent import org.jetbrains.jewel.ui.component.TabData import org.jetbrains.jewel.ui.component.TabStrip import org.jetbrains.jewel.ui.component.VerticalScrollbar @Composable fun PackageSearchInfoPanel( - modifier: Modifier, + modifier: Modifier = Modifier, onLinkClick: (String) -> Unit, onPackageEvent: (PackageListItemEvent) -> Unit, ) = Box(modifier) { val viewModel = viewModel() val tabs by viewModel.tabs.collectAsState() val activeTabTitle by viewModel.activeTabTitleFlow.collectAsState() - val activeTab by derivedStateOf { - tabs.firstOrNull { it.tabTitle == activeTabTitle } - } + // if you use `by derivedStateOf`, the then will fail + val activeTab = derivedStateOf { tabs.firstOrNull { it.tabTitle == activeTabTitle } }.value when { tabs.isEmpty() || activeTab == null -> NoTabsAvailable() - else -> Column(modifier = Modifier.fillMaxSize()) { TabStrip( modifier = Modifier.fillMaxWidth(), - tabs = tabs.map { + tabs = tabs.map { infoPanelContent -> TabData.Default( - selected = activeTabTitle == it.tabTitle, - label = it.tabTitle, + selected = activeTabTitle == infoPanelContent.tabTitle, + content = { SimpleTabContent(label = infoPanelContent.tabTitle, state = it) }, closable = false, - onClick = { viewModel.setActiveTabTitle(it.tabTitle) }, + onClick = { viewModel.setActiveTabTitle(infoPanelContent.tabTitle) }, ) } ) Box(modifier = Modifier.fillMaxWidth()) { Column( modifier = Modifier - .padding(start = 4.dp, top = 4.dp, end = 16.dp, bottom = 12.dp) + .padding(end = PackageSearchMetrics.scrollbarWidth) .verticalScroll(viewModel.scrollState) ) { - val tab = activeTab - if (tab is InfoPanelContent.PackageInfo) { - PackageOverviewTab( - onLinkClick = onLinkClick, - onPackageEvent = onPackageEvent, - content = tab - ) + when (activeTab) { + is InfoPanelContent.PackageInfo -> { + PackageOverviewTab( + onLinkClick = onLinkClick, + onPackageEvent = onPackageEvent, + content = activeTab + ) + } + + is InfoPanelContent.Attributes.FromVariant -> { + HeaderAttributesTab(content = activeTab, scrollState = viewModel.scrollState) + + } + + is InfoPanelContent.Attributes.FromSearch -> { + HeaderAttributesTab(content = activeTab, scrollState = viewModel.scrollState) + } } } VerticalScrollbar( @@ -83,7 +92,10 @@ private fun NoTabsAvailable() { Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { - LabelInfo(PackageSearchBundle.message("packagesearch.ui.toolwindow.packages.details.noItemSelected"), textAlign = TextAlign.Center) + LabelInfo( + PackageSearchBundle.message("packagesearch.ui.toolwindow.packages.details.noItemSelected"), + textAlign = TextAlign.Center + ) } } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/tree/PackageSearchModulesTree.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/tree/PackageSearchModulesTree.kt index 93fcf62c..15565fde 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/tree/PackageSearchModulesTree.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/tree/PackageSearchModulesTree.kt @@ -1,16 +1,18 @@ package com.jetbrains.packagesearch.plugin.ui.panels.tree import androidx.compose.animation.Crossfade -import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -28,8 +30,6 @@ import com.jetbrains.packagesearch.plugin.ui.model.tree.TreeItemModel import com.jetbrains.packagesearch.plugin.ui.model.tree.TreeViewModel import com.jetbrains.packagesearch.plugin.ui.viewModel import org.jetbrains.jewel.foundation.lazy.tree.Tree -import org.jetbrains.jewel.foundation.lazy.tree.buildTree -import org.jetbrains.jewel.foundation.lazy.tree.rememberTreeState import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.Orientation import org.jetbrains.jewel.ui.component.Divider @@ -38,6 +38,7 @@ import org.jetbrains.jewel.ui.component.IconButton import org.jetbrains.jewel.ui.component.LazyTree import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.Tooltip +import org.jetbrains.jewel.ui.component.VerticalScrollbar @Composable fun PackageSearchModulesTree( @@ -76,21 +77,28 @@ fun PackageSearchModulesTree( .toSet() } } - - LazyTree( - modifier = Modifier.padding(top = 4.dp), - tree = tree, - treeState = viewModel.treeState, - onSelectionChange = { - onSelectionChanged( - it.map { it.id } - .filterIsInstance() - .toSet() - ) - }, - ) { item -> - TreeItem(item) + Box { + LazyTree( + modifier = Modifier.padding(top = 4.dp, end = PackageSearchMetrics.scrollbarWidth), + tree = tree, + treeState = viewModel.treeState, + onSelectionChange = { + onSelectionChanged( + it.map { it.id } + .filterIsInstance() + .toSet() + ) + }, + ) { item -> + TreeItem(item) + } + VerticalScrollbar( + adapter = rememberScrollbarAdapter(scrollState = viewModel.lazyListState), + modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd), + ) } + + } @Composable @@ -146,7 +154,7 @@ private fun TreeActionToolbar( modifier = Modifier.padding(5.dp), resource = "icons/intui/toggleOfflineMode.svg", iconClass = IconProvider::class.java, - contentDescription = "Package Search is offline." + contentDescription = "Package FromSearch is offline." ) }, ) @@ -181,7 +189,7 @@ private fun TreeItem(element: Tree.Element) { } if (element.data.hasUpdates) { Icon( - modifier = Modifier.padding(end = 20.dp), + modifier = Modifier.padding(end = 12.dp), resource = "icons/intui/upgradableMark.svg", iconClass = IconProvider::class.java, contentDescription = "" @@ -189,52 +197,52 @@ private fun TreeItem(element: Tree.Element) { } } } - -@Preview -@Composable -private fun TreeItemPreview() { - val items = listOf( - TreeItemModel( - id = PackageSearchModule.Identity("a", ":"), - text = "JetBrains", - hasUpdates = true, - icon = IconProvider.Icon("icons/npm.svg"), - ), - TreeItemModel( - id = PackageSearchModule.Identity("a", ":b"), - text = "Kotlin", - hasUpdates = false, - icon = IconProvider.Icon("icons/maven.svg"), - ), - TreeItemModel( - id = PackageSearchModule.Identity("a", ":c"), - text = "Ktor", - hasUpdates = false, - icon = IconProvider.Icon("icons/cocoapods.svg.svg"), - ), - TreeItemModel( - id = PackageSearchModule.Identity("a", ":c:d"), - text = "Compose", - hasUpdates = true, - icon = IconProvider.Icon("icons/npm.svg"), - ), - ) - val tree = buildTree { - addLeaf(items[0], items[0].id) - addNode(items[1], items[1].id) { - addLeaf(items[2], items[2].id) - } - addLeaf(items[3], items[3].id) - } - LazyTree( - modifier = Modifier.padding(top = 4.dp), - tree = tree, - treeState = rememberTreeState(), - onSelectionChange = {}, - ) { item -> - TreeItem(item) - } -} - - - +// +//@Preview +//@Composable +//private fun TreeItemPreview() { +// val items = listOf( +// TreeItemModel( +// id = PackageSearchModule.Identity("a", ":"), +// text = "JetBrains", +// hasUpdates = true, +// icon = IconProvider.Icon("icons/npm.svg"), +// ), +// TreeItemModel( +// id = PackageSearchModule.Identity("a", ":b"), +// text = "Kotlin", +// hasUpdates = false, +// icon = IconProvider.Icon("icons/maven.svg"), +// ), +// TreeItemModel( +// id = PackageSearchModule.Identity("a", ":c"), +// text = "Ktor", +// hasUpdates = false, +// icon = IconProvider.Icon("icons/cocoapods.svg.svg"), +// ), +// TreeItemModel( +// id = PackageSearchModule.Identity("a", ":c:d"), +// text = "Compose", +// hasUpdates = true, +// icon = IconProvider.Icon("icons/npm.svg"), +// ), +// ) +// val tree = buildTree { +// addLeaf(items[0], items[0].id) +// addNode(items[1], items[1].id) { +// addLeaf(items[2], items[2].id) +// } +// addLeaf(items[3], items[3].id) +// } +// LazyTree( +// modifier = Modifier.padding(top = 4.dp), +// tree = tree, +// treeState = rememberTreeState(), +// onSelectionChange = {}, +// ) { item -> +// TreeItem(item) +// } +//} +// +// +// diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/utils/PackageSearchApiPackageCache.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/utils/PackageSearchApiPackageCache.kt index cd5a7e68..c4758283 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/utils/PackageSearchApiPackageCache.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/utils/PackageSearchApiPackageCache.kt @@ -74,7 +74,7 @@ class PackageSearchApiPackageCache( .toList() .associateBy { it.id } val missingIds = ids - localDatabaseResults.keys - if (missingIds.isNotEmpty() && isOnlineFlow.value) { + if (missingIds.isNotEmpty()) { val networkResults = apiCall(missingIds) // TODO cache also miss in network to avoid pointless empty query if (networkResults.isNotEmpty()) { diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/utils/Utils.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/utils/Utils.kt index 5395f555..801bc323 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/utils/Utils.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/utils/Utils.kt @@ -29,7 +29,7 @@ internal val Project.nativeModules: List get() = ModuleManager.getInstance(this).modules.toList() internal val Project.nativeModulesFlow: FlowWithInitialValue> - get() = messageBus.flow(ModuleListener.TOPIC) { + get() = messageBus.flow(ProjectTopics.MODULES) { object : ModuleListener { override fun modulesAdded(project: Project, modules: NativeModules) { trySend(nativeModules) diff --git a/plugin/src/main/resources/META-INF/plugin.xml b/plugin/src/main/resources/META-INF/plugin.xml index 6c93cece..5f142722 100644 --- a/plugin/src/main/resources/META-INF/plugin.xml +++ b/plugin/src/main/resources/META-INF/plugin.xml @@ -13,6 +13,7 @@ Supports Maven and Gradle projects. org.jetbrains.idea.maven com.intellij.gradle + org.jetbrains.idea.reposearch +