├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── ScalaCI.yml │ ├── auto-approve.yml │ └── scala-steward.yml ├── .gitignore ├── .mergify.yml ├── .scala-steward.conf ├── .scalafmt.conf ├── LICENSE ├── README-CN.md ├── README.md ├── build.sbt ├── docs ├── dependencyTreeConflicts.jpg ├── exclude_conflicts.png ├── gotoAnalyze1.jpg ├── how_to_use_goto.gif ├── how_to_use_goto.mp4 ├── pekkoDependencyTree.png ├── sbtShellUseForReload.jpg ├── scalaJSDependencyTree.png ├── settings.png └── showSize.jpg ├── logo.svg ├── project ├── Commands.scala ├── build.properties ├── plugins.sbt └── sdap.sbt └── src ├── main ├── kotlin │ └── bitlap │ │ └── sbt │ │ └── analyzer │ │ └── jbexternal │ │ ├── AbstractSbtDependencyAnalyzerAction.kt │ │ ├── DependencyAnalyzerManager.kt │ │ ├── DependencyAnalyzerViewImpl.kt │ │ ├── DependencyAnalyzerVirtualFile.kt │ │ ├── SbtDAArtifact.kt │ │ ├── SbtDependencyExternalBundle.kt │ │ ├── package-info.kt │ │ └── util │ │ ├── DependencyUiUtil.kt │ │ ├── ExternalProjectUiUtil.kt │ │ ├── HumanizeUtils.kt │ │ ├── ProjectUtil.kt │ │ ├── ScopeUiUtil.kt │ │ ├── StringUtils.kt │ │ └── UiUtils.kt ├── resources │ ├── META-INF │ │ ├── plugin.xml │ │ └── pluginIcon.svg │ ├── icons │ │ └── sbt_dependency_analyzer.svg │ └── messages │ │ ├── SbtDependencyAnalyzerBundle.properties │ │ ├── SbtDependencyAnalyzerBundle_zh.properties │ │ └── SbtPluginExternalBundle.properties └── scala │ └── bitlap │ └── sbt │ └── analyzer │ ├── Constants.scala │ ├── DependencyScopeEnum.scala │ ├── SbtDependencyAnalyzerBundle.scala │ ├── SbtDependencyAnalyzerConfigurable.scala │ ├── SbtDependencyAnalyzerContributor.scala │ ├── SbtDependencyAnalyzerExtension.scala │ ├── SbtDependencyAnalyzerIcons.scala │ ├── SbtDependencyAnalyzerPanel.form │ ├── SbtDependencyAnalyzerPanel.java │ ├── SbtDependencyAnalyzerPlugin.scala │ ├── SettingsState.scala │ ├── action │ ├── BaseRefreshDependenciesAction.scala │ ├── SbtDependencyAnalyzerAction.scala │ ├── SbtDependencyAnalyzerActionUtil.scala │ ├── SbtDependencyAnalyzerExcludeAction.scala │ ├── SbtDependencyAnalyzerGoToAction.scala │ ├── SbtDependencyAnalyzerOpenConfigAction.scala │ ├── SbtRefreshDependenciesAction.scala │ └── SbtRefreshSnapshotDependenciesAction.scala │ ├── activity │ ├── BaseProjectActivity.scala │ ├── PluginUpdateActivity.scala │ ├── WhatsNew.scala │ └── WhatsNewAction.scala │ ├── model │ ├── AnalyzerException.scala │ ├── ArtifactInfo.scala │ ├── Dependencies.scala │ ├── ModuleContext.scala │ └── Relation.scala │ ├── package.scala │ ├── parser │ ├── AnalyzedDotFileParser.scala │ ├── AnalyzedFileParser.scala │ ├── AnalyzedFileType.scala │ ├── AnalyzedParserFactory.scala │ ├── DotUtil.scala │ └── Graph.scala │ ├── task │ ├── DependencyDotTask.scala │ ├── ModuleNameTask.scala │ ├── OrganizationTask.scala │ ├── RefreshSnapshotsTask.scala │ ├── ReloadTask.scala │ ├── SbtShellDependencyAnalysisTask.scala │ └── SbtShellOutputAnalysisTask.scala │ └── util │ ├── DependencyUtils.scala │ ├── Notifications.scala │ ├── SbtDependencyTraverser.scala │ ├── SbtDependencyUtils.scala │ ├── SbtReimportProject.scala │ ├── SbtUtils.scala │ └── packagesearch │ ├── AddDependencyPreviewWizard.scala │ ├── SbtDependencyModifier.scala │ ├── SbtPossiblePlacesPanel.scala │ └── SbtPossiblePlacesStep.scala └── test ├── resources └── test.dot └── scala └── bitlap └── sbt └── analyzer ├── AnalyzedDotFileParserSpec.scala ├── DotUtilSpec.scala └── SbtShellTaskRegex.scala /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: https://blog.dreamylost.cn/donate/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | ### Preparation action completed 10 | - [ ] I have added `addDependencyTreePlugin` into `plugins.sbt` (sbt.1.4+) or the file `sdap.sbt` exists and contains `addDependencyTreePlugin` 11 | - [ ] I have set `organization := ` for root project and cross-platform 12 | 13 | ### Screenshot of the current sbt Shell 14 | 15 | some images or descriptions 16 | 17 | ### Screenshot of the current dependency tree 18 | 19 | some images or descriptions 20 | 21 | ### Scala platform (scala js, scala jvm, scala native) and version (scala 2.x, scala 3.x) 22 | 23 | some images or descriptions 24 | 25 | ### IDEA version comes from `About Intellij IDEA` 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/ScalaCI.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: [ "*" ] 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up JDK 17 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: temurin 21 | java-version: 17 22 | cache: sbt 23 | - uses: sbt/setup-sbt@v1 24 | - uses: coursier/cache-action@v6 25 | - name: Build 26 | run: sbt compile 27 | 28 | lint: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Set up JDK 17 33 | uses: actions/setup-java@v4 34 | with: 35 | distribution: temurin 36 | java-version: 17 37 | cache: sbt 38 | - uses: sbt/setup-sbt@v1 39 | - uses: coursier/cache-action@v6 40 | - name: Check Style 41 | run: sbt check 42 | 43 | test: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - name: Set up JDK 17 48 | uses: actions/setup-java@v4 49 | with: 50 | distribution: temurin 51 | java-version: 17 52 | cache: sbt 53 | - uses: sbt/setup-sbt@v1 54 | - uses: coursier/cache-action@v6 55 | - name: Test 56 | run: sbt test 57 | 58 | verification: 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v4 62 | - name: Set up JDK 17 63 | uses: actions/setup-java@v4 64 | with: 65 | distribution: temurin 66 | java-version: 17 67 | cache: sbt 68 | - uses: sbt/setup-sbt@v1 69 | - uses: coursier/cache-action@v6 70 | - name: Check Binary Compatibility 71 | run: sbt runPluginVerifier 72 | 73 | 74 | ci: 75 | runs-on: ubuntu-latest 76 | needs: [ build, lint, test, verification] 77 | steps: 78 | - name: Aggregate outcomes 79 | run: echo "build succeeded" 80 | -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | name: Auto approve 2 | on: pull_request_target 3 | 4 | jobs: 5 | auto-approve: 6 | runs-on: ubuntu-latest 7 | permissions: 8 | pull-requests: write 9 | steps: 10 | - uses: hmarr/auto-approve-action@v4 11 | if: github.actor == 'renovate[bot]' || github.actor == 'scala-steward-bitlap[bot]' 12 | with: 13 | github-token: "${{ secrets.GITHUB_TOKEN }}" -------------------------------------------------------------------------------- /.github/workflows/scala-steward.yml: -------------------------------------------------------------------------------- 1 | name: Scala Steward 2 | 3 | # This workflow will launch every day at 00:00 4 | on: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | workflow_dispatch: {} 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | jobs: 14 | scala-steward: 15 | timeout-minutes: 45 16 | runs-on: ubuntu-latest 17 | name: Scala Steward 18 | steps: 19 | - name: Setup sbt 20 | uses: sbt/setup-sbt@v1 21 | - name: Scala Steward 22 | uses: scala-steward-org/scala-steward-action@v2 23 | with: 24 | github-app-id: ${{ secrets.APP_ID }} 25 | github-app-installation-id: ${{ secrets.APP_INSTALLATION_ID }} 26 | github-app-key: ${{ secrets.APP_PRIVATE_KEY }} 27 | github-app-auth-only: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 5 | hs_err_pid* 6 | target/ 7 | .idea/ 8 | .bsp/ -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | merge_conditions: 4 | - "-draft" 5 | merge_method: merge 6 | update_method: rebase 7 | 8 | pull_request_rules: 9 | - name: assign and label scala-steward's PRs 10 | conditions: 11 | - or: 12 | - "author=scala-steward" 13 | - "author=scala-steward-bitlap[bot]" 14 | - "author=renovate[bot]" 15 | actions: 16 | assign: 17 | users: ["@bitlap/intellij-sbt-dependency-analyzer"] 18 | label: 19 | add: ["type: dependencies"] 20 | 21 | - name: merge Scala Steward's PRs 22 | conditions: 23 | - "check-success=ci" 24 | - "#approved-reviews-by>=1" 25 | - or: 26 | - "author=scala-steward" 27 | - "author=scala-steward-bitlap[bot]" 28 | - "author=renovate[bot]" 29 | actions: 30 | queue: 31 | name: default -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.ignore = [ { groupId = "org.scalameta", artifactId = "sbt-scalafmt" } ] -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.7.17" 2 | runner.dialect = scala3 3 | maxColumn = 120 4 | align.preset = more 5 | lineEndings = preserve 6 | align.stripMargin = false 7 | docstrings.style = AsteriskSpace 8 | docstrings.oneline = keep 9 | continuationIndent.defnSite = 2 10 | danglingParentheses.preset = true 11 | spaces { 12 | inImportCurlyBraces = true 13 | } 14 | indentOperator.exemptScope = aloneArgOrBody 15 | includeCurlyBraceInSelectChains = false 16 | align.openParenDefnSite = false 17 | optIn.annotationNewlines = true 18 | rewrite.rules = [SortImports, RedundantBraces] 19 | rewriteTokens = { 20 | "⇒": "=>" 21 | "→": "->" 22 | "←": "<-" 23 | } 24 | 25 | rewrite.rules = [Imports] 26 | rewrite.imports.sort = scalastyle 27 | rewrite.imports.groups = [ 28 | ["java\\..*", "javax\\..*"], 29 | ["scala\\..*"] 30 | ["bitlap\\..*"], 31 | ["org\\..*"], 32 | ["com\\..*"], 33 | ] 34 | rewrite.imports.contiguousGroups = no 35 | newlines.topLevelStatementBlankLines = [ 36 | { 37 | blanks {before = 1} 38 | } 39 | ] -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | Sbt Dependency Analyzer for IntelliJ IDEA 2 | --------- 3 | 4 | logo 5 | 6 | [![Build](https://github.com/bitlap/intellij-sbt-dependency-analyzer/actions/workflows/ScalaCI.yml/badge.svg)](https://github.com/bitlap/intellij-sbt-dependency-analyzer/actions/workflows/ScalaCI.yml) 7 | [![Version](https://img.shields.io/jetbrains/plugin/v/22427-sbt-dependency-analyzer?label=Version)](https://plugins.jetbrains.com/plugin/22427-sbt-dependency-analyzer/versions) 8 | [![JetBrains Plugin Downloads](https://img.shields.io/jetbrains/plugin/d/22427?label=JetBrains%20Plugin%20Downloads)](https://plugins.jetbrains.com/plugin/22427-sbt-dependency-analyzer) 9 | 10 | 中文 | [English](README.md) 11 | 12 | **如果你喜欢这个项目,或者对你有用,[点击](https://github.com/bitlap/intellij-sbt-dependency-analyzer)右上角 ⭐️ Star 支持下吧~** 13 | 14 | ## 特性 15 | 16 | > 本插件从 IntelliJ IDEA 2023.1(Community、Ultimate 或 Android Studio) 开始支持 17 | 18 | - [x] 查看依赖树 19 | - [x] 显示冲突 20 | - [x] 搜索依赖 21 | - [x] 显示模块间依赖关系 22 | - [x] 查看 JAR 包大小 23 | - [x] 依赖定位 24 | - 点击后将跳转到该依赖在`build.sbt`中的位置 25 | - 仅对用户定义依赖可用 26 | - [x] 依赖排除(实验性) 27 | - 选中用户定义依赖下的传递依赖,表示排除;选择中用户定义依赖本身,表示删除该依赖 28 | - 版本`0.5.0-242.21829.142`及以上支持 29 | 30 | 31 | ## 使用说明 32 | 33 | 此插件首次分析失败时将自动生成 `project/sdap.sbt` 文件,并在其中插入一行 `addDependencyTreePlugin` (或 `addSbtPlugin(...)` )。 请勿修改或删除 `project/sdap.sbt` 文件。 34 | 35 | 此插件依赖于 `sbt-dependency-tree`,这是一个第三方插件,但现在已默认集成到 sbt 中(尽管默认情况下未启用,详见 [sbt 问题](https://github.com/sbt/sbt/pull/5880))。 36 | 37 | **让我们看看如何使用它!** 38 | 39 | > 默认快捷键: Ctrl + Shift + L 40 | 41 | ![image](https://plugins.jetbrains.com/files/22427/screenshot_064531dc-a3fa-4a8e-9437-7e76defa1f48) 42 | 43 | ## 更多细节 44 | 45 | 该插件使用以下 sbt 命令。但请放心,插件已经优化,以尽量减少执行的次数:`organization`、`moduleName`、`dependencyDot`、`reload`、`update` 46 | 47 | ## 高级设置 48 | 49 | > 如果不确定,您可以安全地跳过这些配置! 50 | 51 | 通过使用配置,可以显著减少分析等待时间: 52 | 53 | settings 54 | 55 | **文件缓存超时** 56 | 57 | 如果依赖文件(`.dot`)在最近`3600秒`(默认值)内没有修改过,插件将继续使用存在的文件来分析,否则将重新执行`dependencyDot`命令,这是一定程度上的缓存,但在项目首次打开分析图时,缓存可能不生效。 58 | 59 | **组织ID** 60 | 61 | 如果您指定了此值,则将不再使用 `organization` 命令获取项目的组织ID。 62 | 63 | **禁用作用域** 64 | 65 | 如果不需要分析所有作用域,只需禁用您不想要分析的作用域。 66 | 67 | 配置是持久的,并与每个 IntelliJ 项目相关联。 68 | 69 | 与其他插件一样,此插件具有自己的存储位置,即 `.idea/bitlap.sbt.dependency.analyzer.xml`。删除此文件将清除缓存。 70 | 71 | ## 问题排查 72 | 73 | ### "Caused by: java.io.IOException: Could not create lock for ..." 74 | 75 | 由于插件需要使用 sbt shell,打开依赖分析视图并随后使用 IntelliJ IDEA 重新加载或构建项目可能会导致以下问题: 76 | 77 | ``` 78 | Caused by: java.io.IOException: Could not create lock for \\.\pipe\sbt-load5964714308503584069_lock, error 5 79 | ``` 80 | 81 | 为避免此问题,使用 sbt shell 来重新加载或构建项目: 82 | 83 | settings 84 | 85 | ### 无法分析模块之间的依赖关系? 86 | 87 | 请确保您已应用了以下配置之一,以帮助识别正确的模块: 88 | 89 | - 在 [高级设置](#高级设置) 中已配置 `organization`。 90 | - 在 `build.sbt` 中使用 `ThisBuild` 或 `inThisBuild` 设置了 `organization` 值。 91 | 92 | > 注意:不在根项目的`dependsOn`中的子模块不会被分析,依赖为空。 93 | 94 | ## 特别感谢 95 | 96 | 本项目使用 JetBrains IDEA 开发。 感谢 JetBrains 提供的免费许可证。 97 | 98 | 99 | IntelliJ IDEA logo. 100 | 101 | 102 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Sbt Dependency Analyzer for IntelliJ IDEA 2 | --------- 3 | 4 | logo 5 | 6 | [![Build](https://github.com/bitlap/intellij-sbt-dependency-analyzer/actions/workflows/ScalaCI.yml/badge.svg)](https://github.com/bitlap/intellij-sbt-dependency-analyzer/actions/workflows/ScalaCI.yml) 7 | [![Version](https://img.shields.io/jetbrains/plugin/v/22427-sbt-dependency-analyzer?label=Version)](https://plugins.jetbrains.com/plugin/22427-sbt-dependency-analyzer/versions) 8 | [![JetBrains Plugin Downloads](https://img.shields.io/jetbrains/plugin/d/22427?label=JetBrains%20Plugin%20Downloads)](https://plugins.jetbrains.com/plugin/22427-sbt-dependency-analyzer) 9 | 10 | English | [中文](README-CN.md) 11 | 12 | **If you find the Sbt Dependency Analyzer interesting, please ⭐ [Star](https://github.com/bitlap/intellij-sbt-dependency-analyzer) it at the top of the GitHub page to support us.** 13 | 14 | ## Features 15 | 16 | > The plugin is available since IntelliJ IDEA 2023.1 (Community, Ultimate and Android Studio) 17 | 18 | - [x] View Dependency Tree 19 | - [x] Show Conflicts 20 | - [x] Search Dependencies 21 | - [x] Show Dependencies Between Modules 22 | - [x] Show JAR Size 23 | - [x] Goto Dependency 24 | - Clicking on them will take you to the location of the dependency in `build.sbt` 25 | - Available only for user-defined dependencies 26 | - [x] Dependency Exclusion (Experimental) 27 | - Selecting transitive dependencies in user-defined dependencies indicates exclusion, while selecting user-defined dependencies indicates deletion itself 28 | - Available since Sbt Dependency Analyzer `0.5.0-242.21829.142` 29 | 30 | ## Usage Instructions 31 | 32 | This plugin will automatically generate `project/sdap.sbt` when the first analysis fails and insert the `addDependencyTreePlugin` (or `addSbtPlugin(...)`) statement into it. If generated, please do not modify or delete `project/sdap.sbt`. 33 | 34 | This plugin relies on `sbt-dependency-tree`, a third-party plugin, which is now integrated into sbt by default (although it won't be enabled by default, as explained in this [sbt issue](https://github.com/sbt/sbt/pull/5880)). 35 | 36 | **Let's explore how to use it!** 37 | 38 | > Default shortcut: Ctrl + Shift + L 39 | 40 | ![image](https://plugins.jetbrains.com/files/22427/screenshot_064531dc-a3fa-4a8e-9437-7e76defa1f48) 41 | 42 | ## More Details 43 | 44 | The plugin utilizes the following sbt commands. However, rest assured that the plugin has been optimized to minimize the number of executions as much as possible: `organization`,`moduleName`,`dependencyDot`,`reload`,`update` 45 | 46 | ## Advanced Setup 47 | 48 | > If you are uncertain, you can safely skip these configurations! 49 | 50 | By utilizing configurations, analysis wait times can be significantly reduced: 51 | 52 | settings 53 | 54 | **File Cache Timeout** 55 | 56 | If the dependent file (`.dot`) has not been modified within the last `3600 seconds` (default value), the plugin will continue to use the existing file for analysis, 57 | otherwise the `dependencyDot` command will be executed, which is a certain degree of caching, but the caching may not take effect when the project first opens the analysis graph. 58 | 59 | **Organization** 60 | 61 | If you specify this value, the `organization` command will not be used to retrieve your project's organization. 62 | 63 | **Disable Scopes** 64 | 65 | If you do not need to analyze all scopes, simply disable the scope(s) you wish to skip. 66 | 67 | Configurations are persistent and associated with each IntelliJ project. 68 | 69 | Like other plugins, this one maintains its own storage in `.idea/bitlap.sbt.dependency.analyzer.xml`. Deleting this file will clear the cache. 70 | 71 | ## Troubleshooting Issues 72 | 73 | ### "Caused by: java.io.IOException: Could not create lock for ..." 74 | 75 | Due to the plugin's requirement to use sbt shell, opening the dependency analysis view and subsequently using IntelliJ IDEA to reload or build the project may lead to the following issue: 76 | ``` 77 | Caused by: java.io.IOException: Could not create lock for \\.\pipe\sbt-load5964714308503584069_lock, error 5 78 | ``` 79 | 80 | To avoid this problem, utilize sbt shell for reloading or building the project: 81 | 82 | settings 83 | 84 | ### Unable to analyze dependencies between modules? 85 | 86 | Ensure that you have applied one of the following settings to help identify the correct module: 87 | - The `organization` in [Advanced Setup](#advanced-setup) has been configured. 88 | - The `organization` value has been set in `build.sbt` via `ThisBuild` or `inThisBuild`. 89 | 90 | > Note: Sub modules that are not in the `dependsOn` of the root project will not be parsed and their dependencies will be empty. 91 | 92 | ## JetBrains Support 93 | 94 | This project is developed using JetBrains IDEA. 95 | Thanks to JetBrains for providing me with a free license, which is a strong support for me. 96 | 97 | 98 | IntelliJ IDEA logo. 99 | 100 | 101 |
102 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import org.jetbrains.sbtidea.Keys.* 2 | import org.jetbrains.sbtidea.verifier.FailureLevel 3 | 4 | lazy val scala3Version = "3.6.4" 5 | lazy val logbackVersion = "1.5.18" 6 | lazy val graphvizVersion = "0.18.1" 7 | lazy val joorVersion = "0.9.15" 8 | lazy val scalatestVersion = "3.2.19" 9 | lazy val pluginVerifierVersion = "1.305" 10 | lazy val ktVersion = "2.1.0" 11 | lazy val jbAnnotVersion = "26.0.2" 12 | 13 | // https://youtrack.jetbrains.com/articles/IDEA-A-2100661679/IntelliJ-IDEA-2023.3-Latest-Builds 14 | // NOTE: Latest-Builds 233 15 | lazy val intellijVersion = "251.23536.34" 16 | lazy val pluginVersion = s"0.7.0-$intellijVersion" 17 | 18 | ThisBuild / version := pluginVersion 19 | 20 | inThisBuild( 21 | List( 22 | homepage := Some(url("https://github.com/bitlap/intellij-sbt-dependency-analyzer")), 23 | developers := List( 24 | Developer( 25 | id = "jxnu-liguobin", 26 | name = "梦境迷离", 27 | email = "dreamylost@outlook.com", 28 | url = url("https://blog.dreamylost.cn") 29 | ), 30 | Developer( 31 | id = "IceMimosa", 32 | name = "IceMimosa", 33 | email = "chk19940609@gmail.com", 34 | url = url("http://patamon.me") 35 | ) 36 | ) 37 | ) 38 | ) 39 | 40 | lazy val `sbt-dependency-analyzer` = (project in file(".")) 41 | .enablePlugins(SbtIdeaPlugin) 42 | .settings( 43 | scalaVersion := scala3Version, 44 | organization := "org.bitlap", 45 | scalacOptions ++= Seq( 46 | "-deprecation", 47 | "-Xfatal-warnings" 48 | /** , "-rewrite", "-source:3.4-migration"* */ 49 | ), 50 | version := (ThisBuild / version).value, 51 | ThisBuild / intellijPluginName := "Sbt Dependency Analyzer", 52 | ThisBuild / intellijBuild := intellijVersion, 53 | ThisBuild / intellijPlatform := (Global / intellijPlatform).??(IntelliJPlatform.IdeaCommunity).value, 54 | signPluginOptions := signPluginOptions.value.copy( 55 | enabled = true, 56 | certFile = Some(file(sys.env.getOrElse("PLUGIN_SIGN_KEY", "/Users/liguobin/chain.crt"))), 57 | privateKeyFile = Some(file(sys.env.getOrElse("PLUGIN_SIGN_CERT", "/Users/liguobin/private.pem"))), 58 | keyPassphrase = Some(sys.env.getOrElse("PLUGIN_SIGN_KEY_PWD", "123456")) 59 | ), 60 | pluginVerifierOptions := pluginVerifierOptions.value.copy( 61 | version = pluginVerifierVersion, // use a specific verifier version 62 | offline = true, // forbid the verifier from reaching the internet 63 | failureLevels = Set(FailureLevel.COMPATIBILITY_PROBLEMS, FailureLevel.COMPATIBILITY_WARNINGS) 64 | ), 65 | Global / intellijAttachSources := true, 66 | intellijPlugins ++= Seq("com.intellij.java", "com.intellij.java-i18n", "org.intellij.scala").map(_.toPlugin), 67 | Compile / unmanagedResourceDirectories += baseDirectory.value / "src" / "main" / "resources", 68 | Test / unmanagedResourceDirectories += baseDirectory.value / "src" / "test" / "resources", 69 | patchPluginXml := pluginXmlOptions { xml => 70 | xml.version = pluginVersion 71 | // xml.pluginDescription = IO.read(baseDirectory.value / "src" / "main" / "resources" / "patch" / "description.html") 72 | // xml.changeNotes = IO.read(baseDirectory.value / "src" / "main" / "resources" / "patch" / "change.html") 73 | }, 74 | publish / skip := true, 75 | commands ++= Commands.value, 76 | libraryDependencies ++= Seq( 77 | "guru.nidi" % "graphviz-java-min-deps" % graphvizVersion, 78 | "ch.qos.logback" % "logback-classic" % logbackVersion, 79 | "org.jooq" % "joor" % joorVersion, 80 | "org.scalatest" %% "scalatest" % scalatestVersion % Test, 81 | "org.jetbrains" % "annotations" % jbAnnotVersion 82 | ), 83 | kotlinVersion := ktVersion, 84 | Compile / unmanagedSourceDirectories += baseDirectory.value / "src" / "main" / "kotlin", 85 | packageLibraryMappings ++= Seq( 86 | "org.jetbrains.kotlin" % ".*" % ".*" -> None, 87 | "org.jetbrains" % ".*" % ".*" -> None 88 | ) 89 | ) 90 | -------------------------------------------------------------------------------- /docs/dependencyTreeConflicts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlap/intellij-sbt-dependency-analyzer/78195d210caf9ab70d828a361cec1c4d98fd4e52/docs/dependencyTreeConflicts.jpg -------------------------------------------------------------------------------- /docs/exclude_conflicts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlap/intellij-sbt-dependency-analyzer/78195d210caf9ab70d828a361cec1c4d98fd4e52/docs/exclude_conflicts.png -------------------------------------------------------------------------------- /docs/gotoAnalyze1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlap/intellij-sbt-dependency-analyzer/78195d210caf9ab70d828a361cec1c4d98fd4e52/docs/gotoAnalyze1.jpg -------------------------------------------------------------------------------- /docs/how_to_use_goto.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlap/intellij-sbt-dependency-analyzer/78195d210caf9ab70d828a361cec1c4d98fd4e52/docs/how_to_use_goto.gif -------------------------------------------------------------------------------- /docs/how_to_use_goto.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlap/intellij-sbt-dependency-analyzer/78195d210caf9ab70d828a361cec1c4d98fd4e52/docs/how_to_use_goto.mp4 -------------------------------------------------------------------------------- /docs/pekkoDependencyTree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlap/intellij-sbt-dependency-analyzer/78195d210caf9ab70d828a361cec1c4d98fd4e52/docs/pekkoDependencyTree.png -------------------------------------------------------------------------------- /docs/sbtShellUseForReload.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlap/intellij-sbt-dependency-analyzer/78195d210caf9ab70d828a361cec1c4d98fd4e52/docs/sbtShellUseForReload.jpg -------------------------------------------------------------------------------- /docs/scalaJSDependencyTree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlap/intellij-sbt-dependency-analyzer/78195d210caf9ab70d828a361cec1c4d98fd4e52/docs/scalaJSDependencyTree.png -------------------------------------------------------------------------------- /docs/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlap/intellij-sbt-dependency-analyzer/78195d210caf9ab70d828a361cec1c4d98fd4e52/docs/settings.png -------------------------------------------------------------------------------- /docs/showSize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlap/intellij-sbt-dependency-analyzer/78195d210caf9ab70d828a361cec1c4d98fd4e52/docs/showSize.jpg -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project/Commands.scala: -------------------------------------------------------------------------------- 1 | import sbt.Command 2 | 3 | object Commands { 4 | 5 | val FmtSbtCommand = Command.command("fmt")(state => "scalafmtSbt" :: "scalafmtAll" :: state) 6 | 7 | val FmtSbtCheckCommand = 8 | Command.command("check")(state => "scalafmtSbtCheck" :: "scalafmtCheckAll" :: state) 9 | 10 | val value = Seq( 11 | FmtSbtCommand, 12 | FmtSbtCheckCommand 13 | ) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.2 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / resolvers ++= Seq( 2 | "Sonatype OSS Snapshots" at "https://s01.oss.sonatype.org/content/repositories/snapshots", 3 | "Sonatype OSS Releases" at "https://s01.oss.sonatype.org/content/repositories/releases" 4 | ) 5 | addSbtPlugin("org.jetbrains" % "sbt-ide-settings" % "1.1.0") 6 | addSbtPlugin("org.jetbrains" % "sbt-idea-plugin" % "4.1.11") 7 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") 8 | addSbtPlugin("org.bitlap" % "sbt-kotlin-plugin" % "4.0.1") 9 | -------------------------------------------------------------------------------- /project/sdap.sbt: -------------------------------------------------------------------------------- 1 | // -- This file was mechanically generated by Sbt Dependency Analyzer Plugin: Do not edit! -- // 2 | addDependencyTreePlugin 3 | -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/AbstractSbtDependencyAnalyzerAction.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 2 | 3 | package bitlap.sbt.analyzer.jbexternal 4 | 5 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerAction 6 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerDependency 7 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerView 8 | 9 | 10 | import com.intellij.openapi.actionSystem.AnActionEvent 11 | import com.intellij.openapi.module.Module 12 | 13 | abstract class BaseDependencyAnalyzerAction : DependencyAnalyzerAction() { 14 | 15 | override fun actionPerformed(e: AnActionEvent) { 16 | val project = e.project ?: return 17 | val systemId = getSystemId(e) ?: return 18 | val dependencyAnalyzerManager = DependencyAnalyzerManager.getInstance(project) 19 | val dependencyAnalyzerView = dependencyAnalyzerManager.getOrCreate(systemId) 20 | setSelectedState(dependencyAnalyzerView, e) 21 | } 22 | } 23 | 24 | abstract class AbstractSbtDependencyAnalyzerAction : BaseDependencyAnalyzerAction() { 25 | 26 | abstract fun getSelectedData(e: AnActionEvent): Data? 27 | 28 | abstract fun getModule(e: AnActionEvent, selectedData: Data): Module? 29 | 30 | abstract fun getDependencyData(e: AnActionEvent, selectedData: Data): DependencyAnalyzerDependency.Data? 31 | 32 | abstract fun getDependencyScope(e: AnActionEvent, selectedData: Data): String? 33 | 34 | override fun isEnabledAndVisible(e: AnActionEvent): Boolean { 35 | val selectedData = getSelectedData(e) ?: return false 36 | return getModule(e, selectedData) != null 37 | } 38 | 39 | override fun setSelectedState(view: DependencyAnalyzerView, e: AnActionEvent) { 40 | val selectedData = getSelectedData(e) ?: return 41 | val module = getModule(e, selectedData) ?: return 42 | val dependencyData = getDependencyData(e, selectedData) 43 | val dependencyScope = getDependencyScope(e, selectedData) 44 | if (dependencyData != null && dependencyScope != null) { 45 | view.setSelectedDependency(module, dependencyData, dependencyScope) 46 | } else if (dependencyData != null) { 47 | view.setSelectedDependency(module, dependencyData) 48 | } else { 49 | view.setSelectedExternalProject(module) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerManager.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. 2 | package bitlap.sbt.analyzer.jbexternal 3 | 4 | import bitlap.sbt.analyzer.jbexternal.util.whenDisposed 5 | 6 | import com.intellij.openapi.components.Service 7 | import com.intellij.openapi.components.service 8 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerExtension 9 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerView 10 | import com.intellij.openapi.externalSystem.model.ProjectSystemId 11 | import com.intellij.openapi.fileEditor.FileEditorManager 12 | import com.intellij.openapi.project.Project 13 | 14 | @Service(Service.Level.PROJECT) 15 | class DependencyAnalyzerManager(private val project: Project) { 16 | 17 | private val files = HashMap() 18 | 19 | fun getOrCreate(systemId: ProjectSystemId): DependencyAnalyzerView { 20 | val fileEditorManager = FileEditorManager.getInstance(project) 21 | val file = files.getOrPut(systemId) { 22 | DependencyAnalyzerVirtualFile(project, systemId).also { file -> 23 | DependencyAnalyzerExtension.createExtensionDisposable(systemId, project).also { extensionDisposable -> 24 | extensionDisposable.whenDisposed { 25 | fileEditorManager.closeFile(file) 26 | files.remove(systemId) 27 | } 28 | } 29 | } 30 | } 31 | fileEditorManager.openFile(file, true, true) 32 | return requireNotNull(file.getViews().firstOrNull()) { 33 | "DependencyAnalyzerView should be created during file open" 34 | } 35 | } 36 | 37 | companion object { 38 | @JvmStatic 39 | fun getInstance(project: Project) = project.service() 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerVirtualFile.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. 2 | package bitlap.sbt.analyzer.jbexternal 3 | 4 | import com.intellij.icons.AllIcons 5 | import com.intellij.ide.actions.SplitAction 6 | import com.intellij.ide.plugins.UIComponentFileEditor 7 | import com.intellij.ide.plugins.UIComponentVirtualFile 8 | import com.intellij.ide.plugins.UIComponentVirtualFile.Content 9 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerView 10 | import com.intellij.openapi.externalSystem.model.ProjectSystemId 11 | import com.intellij.openapi.externalSystem.util.ExternalSystemBundle 12 | import com.intellij.openapi.project.Project 13 | import com.intellij.util.containers.DisposableWrapperList 14 | 15 | /** 16 | * https://github.com/JetBrains/intellij-community/blob/idea/233.11799.300/platform/external-system-impl/src/com/intellij/openapi/externalSystem/dependency/analyzer/DependencyAnalyzerVirtualFile.kt 17 | */ 18 | internal class DependencyAnalyzerVirtualFile(private val project: Project, private val systemId: ProjectSystemId) : 19 | UIComponentVirtualFile( 20 | ExternalSystemBundle.message("external.system.dependency.analyzer.editor.tab.name"), 21 | AllIcons.Actions.DependencyAnalyzer 22 | ) { 23 | private val views = DisposableWrapperList() 24 | 25 | fun getViews(): List = views.toList() 26 | 27 | override fun createContent(editor: UIComponentFileEditor): Content { 28 | val view = DependencyAnalyzerViewImpl(project, systemId, editor) 29 | views.add(view, editor) 30 | return Content { view.createComponent() } 31 | } 32 | 33 | init { 34 | putUserData(SplitAction.FORBID_TAB_SPLIT, true) 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/SbtDAArtifact.kt: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.jbexternal 2 | 3 | import com.intellij.openapi.util.UserDataHolderBase 4 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerDependency as Dependency 5 | 6 | data class SbtDAArtifact( 7 | override val groupId: String, 8 | override val artifactId: String, 9 | override val version: String, 10 | val size: Long, 11 | val totalSize: Long 12 | ) : UserDataHolderBase(), Dependency.Data.Artifact { 13 | override fun toString() = "$groupId:$artifactId:$version" 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/SbtDependencyExternalBundle.kt: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.jbexternal 2 | 3 | import com.intellij.DynamicBundle 4 | import org.jetbrains.annotations.* 5 | 6 | class SbtDependencyExternalBundle : DynamicBundle(BUNDLE) { 7 | companion object { 8 | const val BUNDLE = "messages.SbtPluginExternalBundle" 9 | private val INSTANCE = SbtDependencyExternalBundle() 10 | 11 | @Nls 12 | fun message(@NotNull @PropertyKey(resourceBundle = BUNDLE) key: String, @NotNull vararg params: Any): String = 13 | INSTANCE.getMessage(key, params) 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/package-info.kt: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.jbexternal 2 | 3 | /** 4 | * NOTE: Kotlin code can only be referenced and cannot refer to code in src/main/scala 5 | * The code for this package is almost copied from https://github.com/JetBrains/intellij-community 6 | */ 7 | -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/DependencyUiUtil.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. 2 | package bitlap.sbt.analyzer.jbexternal.util 3 | 4 | import javax.swing.JList 5 | import javax.swing.JTree 6 | import javax.swing.ListModel 7 | import javax.swing.tree.DefaultMutableTreeNode 8 | import javax.swing.tree.TreeModel 9 | 10 | import bitlap.sbt.analyzer.jbexternal.SbtDAArtifact 11 | 12 | import com.intellij.icons.AllIcons 13 | import com.intellij.openapi.actionSystem.DataSink 14 | import com.intellij.openapi.actionSystem.UiDataProvider 15 | import com.intellij.openapi.application.invokeLater 16 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerView 17 | import com.intellij.openapi.externalSystem.util.ExternalSystemBundle 18 | import com.intellij.openapi.observable.properties.* 19 | import com.intellij.openapi.observable.util.bind 20 | import com.intellij.openapi.observable.util.transform 21 | import com.intellij.openapi.observable.util.whenTreeChanged 22 | import com.intellij.openapi.ui.asSequence 23 | import com.intellij.openapi.util.NlsSafe 24 | import com.intellij.ui.* 25 | import com.intellij.ui.SimpleTextAttributes.GRAYED_ATTRIBUTES 26 | import com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES 27 | import com.intellij.ui.components.JBList 28 | import com.intellij.ui.treeStructure.SimpleTree 29 | import com.intellij.util.ui.ListUiUtil 30 | import com.intellij.util.ui.tree.TreeUtil 31 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerDependency as Dependency 32 | 33 | 34 | internal fun Dependency.Data.getDisplayText(showGroupId: Boolean): @NlsSafe String = when (this) { 35 | is Dependency.Data.Module -> name 36 | is Dependency.Data.Artifact -> when (showGroupId) { 37 | true -> "$groupId:$artifactId:$version" 38 | else -> "$artifactId:$version" 39 | } 40 | } 41 | 42 | private fun SimpleColoredComponent.customizeCellRenderer( 43 | group: DependencyGroup, showGroupId: Boolean, showSize: Boolean 44 | ) { 45 | icon = when { 46 | group.hasWarnings -> AllIcons.General.Warning 47 | else -> when (group.data) { 48 | is Dependency.Data.Module -> AllIcons.Nodes.Module 49 | is Dependency.Data.Artifact -> AllIcons.Nodes.PpLib 50 | } 51 | } 52 | val dataText = group.data.getDisplayText(showGroupId) 53 | append(dataText, if (group.isOmitted) GRAYED_ATTRIBUTES else REGULAR_ATTRIBUTES) 54 | val scopes = group.variances.map { it.scope.name }.toSet() 55 | val scopesText = scopes.singleOrNull() ?: ExternalSystemBundle.message( 56 | "external.system.dependency.analyzer.scope.n", scopes.size 57 | ) 58 | append(" ($scopesText)", GRAYED_ATTRIBUTES) 59 | 60 | if (showSize) { 61 | when (group.data) { 62 | is SbtDAArtifact -> { 63 | val selfSize = (group.data as SbtDAArtifact).size.formatAsFileSize 64 | val total = (group.data as SbtDAArtifact).totalSize.formatAsFileSize 65 | if (selfSize == total) { 66 | // no child nodes 67 | if ((group.data as SbtDAArtifact).size.toInt() != 0) { 68 | append( 69 | " - ($selfSize)", GRAYED_ATTRIBUTES 70 | ) 71 | } 72 | } else if ((group.data as SbtDAArtifact).size.toInt() == 0) { 73 | // may have been evicted away by conflicts 74 | append( 75 | " - $total", GRAYED_ATTRIBUTES 76 | ) 77 | } else { 78 | append( 79 | " - $total ($selfSize)", GRAYED_ATTRIBUTES 80 | ) 81 | } 82 | } 83 | 84 | else -> return 85 | } 86 | } 87 | } 88 | 89 | internal abstract class AbstractDependencyList( 90 | model: ListModel 91 | ) : JBList(model), UiDataProvider { 92 | 93 | private val dependencyProperty = AtomicProperty(null) 94 | private val dependencyGroupProperty = AtomicProperty(null) 95 | 96 | fun bindDependency(property: ObservableMutableProperty) = apply { 97 | dependencyProperty.bind(property) 98 | } 99 | 100 | override fun uiDataSnapshot(sink: DataSink) { 101 | sink[DependencyAnalyzerView.DEPENDENCY] = dependencyProperty.get() 102 | sink[DependencyAnalyzerView.DEPENDENCIES] = dependencyGroupProperty.get()?.variances 103 | } 104 | 105 | init { 106 | bind(dependencyGroupProperty) 107 | dependencyGroupProperty.bind(dependencyProperty.transform({ dependency -> 108 | model.asSequence().find { it.data == dependency?.data } 109 | }, { it?.dependency })) 110 | } 111 | } 112 | 113 | internal abstract class AbstractDependencyTree( 114 | model: TreeModel 115 | ) : SimpleTree(model), UiDataProvider { 116 | 117 | private val dependencyProperty = AtomicProperty(null) 118 | private val dependencyGroupProperty = AtomicProperty(null) 119 | 120 | fun bindDependency(property: ObservableMutableProperty) = apply { 121 | dependencyProperty.bind(property) 122 | } 123 | 124 | override fun uiDataSnapshot(sink: DataSink) { 125 | sink[DependencyAnalyzerView.DEPENDENCY] = dependencyProperty.get() 126 | sink[DependencyAnalyzerView.DEPENDENCIES] = dependencyGroupProperty.get()?.variances 127 | } 128 | 129 | init { 130 | bind(dependencyGroupProperty) 131 | dependencyGroupProperty.bind(dependencyProperty.transform({ dependency -> 132 | model.asSequence().map { it.userObject as DependencyGroup } 133 | .find { it.data == dependency?.data && dependency.parent in it.parents } 134 | }, { it?.dependency })) 135 | } 136 | } 137 | 138 | internal open class DependencyList( 139 | model: ListModel, 140 | showGroupIdProperty: ObservableProperty, 141 | showSizeProperty: ObservableProperty, 142 | ) : AbstractDependencyList(model) { 143 | init { 144 | ListUiUtil.Selection.installSelectionOnRightClick(this) 145 | PopupHandler.installPopupMenu( 146 | this, "ExternalSystem.DependencyAnalyzer.DependencyListGroup", DependencyAnalyzerView.ACTION_PLACE 147 | ) 148 | setCellRenderer(DependencyListRenderer(showGroupIdProperty, showSizeProperty)) 149 | } 150 | } 151 | 152 | internal open class DependencyTree( 153 | model: TreeModel, 154 | showGroupIdProperty: ObservableProperty, 155 | showSizeProperty: ObservableProperty, 156 | ) : AbstractDependencyTree(model) { 157 | init { 158 | PopupHandler.installPopupMenu( 159 | this, "ExternalSystem.DependencyAnalyzer.DependencyTreeGroup", DependencyAnalyzerView.ACTION_PLACE 160 | ) 161 | setCellRenderer(DependencyTreeRenderer(showGroupIdProperty, showSizeProperty)) 162 | } 163 | } 164 | 165 | internal open class UsagesTree( 166 | model: TreeModel, 167 | showGroupIdProperty: ObservableProperty, 168 | showSizeProperty: ObservableProperty, 169 | ) : AbstractDependencyTree(model) { 170 | init { 171 | PopupHandler.installPopupMenu( 172 | this, "ExternalSystem.DependencyAnalyzer.UsagesTreeGroup", DependencyAnalyzerView.ACTION_PLACE 173 | ) 174 | setCellRenderer(UsagesTreeRenderer(showGroupIdProperty, showSizeProperty)) 175 | whenTreeChanged { 176 | invokeLater { 177 | TreeUtil.expandAll(this) 178 | } 179 | } 180 | } 181 | } 182 | 183 | private class DependencyListRenderer( 184 | private val showGroupIdProperty: ObservableProperty, 185 | private val showSizeProperty: ObservableProperty, 186 | ) : ColoredListCellRenderer() { 187 | override fun customizeCellRenderer( 188 | list: JList, value: DependencyGroup?, index: Int, selected: Boolean, hasFocus: Boolean 189 | ) { 190 | val group = value ?: return 191 | customizeCellRenderer(group, showGroupIdProperty.get(), showSizeProperty.get()) 192 | } 193 | } 194 | 195 | private class DependencyTreeRenderer( 196 | private val showGroupIdProperty: ObservableProperty, 197 | private val showSizeProperty: ObservableProperty 198 | ) : ColoredTreeCellRenderer() { 199 | override fun customizeCellRenderer( 200 | tree: JTree, value: Any?, selected: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean 201 | ) { 202 | val node = value as? DefaultMutableTreeNode ?: return 203 | val group = node.userObject as? DependencyGroup ?: return 204 | customizeCellRenderer(group, showGroupIdProperty.get(), showSizeProperty.get()) 205 | } 206 | } 207 | 208 | private class UsagesTreeRenderer( 209 | private val showGroupIdProperty: ObservableProperty, 210 | private val showSizeProperty: ObservableProperty 211 | ) : ColoredTreeCellRenderer() { 212 | override fun customizeCellRenderer( 213 | tree: JTree, value: Any?, selected: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean 214 | ) { 215 | val node = value as? DefaultMutableTreeNode ?: return 216 | val group = node.userObject as? DependencyGroup ?: return 217 | customizeCellRenderer(group, showGroupIdProperty.get(), showSizeProperty.get()) 218 | val warning = group.warnings.firstOrNull() 219 | if (warning != null) { 220 | append(" ${warning.message}", SimpleTextAttributes.ERROR_ATTRIBUTES) 221 | } 222 | } 223 | } 224 | 225 | internal class DependencyGroup(val variances: List) { 226 | val dependency by lazy { variances.find { !it.isOmitted } ?: variances.first() } 227 | val data by lazy { dependency.data } 228 | val scopes by lazy { variances.map { it.scope }.toSet() } 229 | val parents by lazy { variances.map { it.parent }.toSet() } 230 | val warnings by lazy { variances.flatMap { it.warnings } } 231 | val isOmitted by lazy { variances.all { it.isOmitted } } 232 | val hasWarnings by lazy { variances.any { it.hasWarnings } } 233 | 234 | override fun toString() = data.toString() 235 | 236 | companion object { 237 | internal val Dependency.isOmitted: Boolean 238 | get() = status.any { it is Dependency.Status.Omitted } 239 | 240 | internal val Dependency.warnings: List 241 | get() = status.filterIsInstance() 242 | 243 | internal val Dependency.hasWarnings: Boolean 244 | get() = warnings.isNotEmpty() 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/ExternalProjectUiUtil.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. 2 | package bitlap.sbt.analyzer.jbexternal.util 3 | 4 | import java.awt.Component 5 | import javax.swing.* 6 | 7 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerProject 8 | import com.intellij.openapi.externalSystem.ui.ExternalSystemIconProvider 9 | import com.intellij.openapi.externalSystem.util.ExternalSystemBundle 10 | import com.intellij.openapi.observable.properties.ObservableMutableProperty 11 | import com.intellij.openapi.observable.util.bind 12 | import com.intellij.openapi.ui.popup.JBPopup 13 | import com.intellij.openapi.ui.popup.JBPopupFactory 14 | import com.intellij.openapi.observable.util.whenItemSelected 15 | import com.intellij.openapi.observable.util.whenMousePressed 16 | import com.intellij.ui.ListUtil 17 | import com.intellij.ui.components.DropDownLink 18 | import com.intellij.ui.components.JBList 19 | import com.intellij.util.ui.EmptyIcon 20 | import com.intellij.util.ui.JBUI 21 | 22 | @Suppress("DEPRECATION") 23 | internal class ExternalProjectSelector( 24 | property: ObservableMutableProperty, 25 | externalProjects: List, 26 | iconProvider: ExternalSystemIconProvider 27 | ) : JPanel() { 28 | 29 | private val projectIcon = if (iconProvider.projectIcon is EmptyIcon) { 30 | PROJECT_ICON 31 | } else { 32 | iconProvider.projectIcon 33 | } 34 | 35 | init { 36 | val dropDownLink = ExternalProjectDropDownLink(property, externalProjects).apply { 37 | border = JBUI.Borders.empty(BORDER, ICON_TEXT_GAP / 2, BORDER, BORDER) 38 | } 39 | val label = JLabel(projectIcon).apply { border = JBUI.Borders.empty(BORDER, BORDER, BORDER, ICON_TEXT_GAP / 2) } 40 | .apply { labelFor = dropDownLink } 41 | 42 | layout = com.intellij.ide.plugins.newui.HorizontalLayout(0) 43 | border = JBUI.Borders.empty() 44 | add(label) 45 | add(dropDownLink) 46 | } 47 | 48 | private fun createPopup( 49 | externalProjects: List, onChange: (DependencyAnalyzerProject) -> Unit 50 | ): JBPopup { 51 | val content = 52 | ExternalProjectPopupContent(externalProjects).apply { whenMousePressed { onChange(selectedValue) } } 53 | return JBPopupFactory.getInstance().createComponentPopupBuilder(content, null).createPopup() 54 | .apply { content.whenMousePressed(listener = ::closeOk) } 55 | } 56 | 57 | private inner class ExternalProjectPopupContent(externalProject: List) : 58 | JBList() { 59 | init { 60 | model = createDefaultListModel(externalProject) 61 | border = emptyListBorder() 62 | cellRenderer = ExternalProjectRenderer() 63 | selectionMode = ListSelectionModel.SINGLE_SELECTION 64 | ListUtil.installAutoSelectOnMouseMove(this) 65 | setupListPopupPreferredWidth(this) 66 | } 67 | } 68 | 69 | private inner class ExternalProjectRenderer : ListCellRenderer { 70 | override fun getListCellRendererComponent( 71 | list: JList, 72 | value: DependencyAnalyzerProject?, 73 | index: Int, 74 | isSelected: Boolean, 75 | cellHasFocus: Boolean 76 | ): Component { 77 | return JLabel().apply { if (value != null) icon = projectIcon } 78 | .apply { if (value != null) text = value.title }.apply { border = emptyListCellBorder(list, index) } 79 | .apply { iconTextGap = JBUI.scale(ICON_TEXT_GAP) } 80 | .apply { background = if (isSelected) list.selectionBackground else list.background } 81 | .apply { foreground = if (isSelected) list.selectionForeground else list.foreground } 82 | .apply { isOpaque = true }.apply { isEnabled = list.isEnabled }.apply { font = list.font } 83 | } 84 | } 85 | 86 | private inner class ExternalProjectDropDownLink( 87 | property: ObservableMutableProperty, 88 | externalProjects: List, 89 | ) : DropDownLink(property.get(), 90 | { createPopup(externalProjects, it::selectedItem.setter) }) { 91 | override fun popupPoint() = super.popupPoint().apply { x += insets.left }.apply { x -= JBUI.scale(BORDER) } 92 | .apply { x -= projectIcon.iconWidth }.apply { x -= JBUI.scale(ICON_TEXT_GAP) } 93 | 94 | override fun itemToString(item: DependencyAnalyzerProject?): String = when (item) { 95 | null -> ExternalSystemBundle.message("external.system.dependency.analyzer.projects.empty") 96 | else -> item.title 97 | } 98 | 99 | init { 100 | autoHideOnDisable = false 101 | foreground = JBUI.CurrentTheme.Label.foreground() 102 | whenItemSelected { text = itemToString(selectedItem) } 103 | bind(property) 104 | } 105 | } 106 | } 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/HumanizeUtils.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package bitlap.sbt.analyzer.jbexternal.util 4 | 5 | import java.util.concurrent.TimeUnit 6 | import kotlin.math.log2 7 | import kotlin.math.pow 8 | 9 | // Also see: 10 | // https://developer.android.com/reference/android/text/format/DateUtils.html#formatElapsedTime(long) 11 | val Long.formatMsAsDuration: String 12 | get() { 13 | fun normalize(number: Long): String = String.format("%02d", number) 14 | 15 | val seconds = TimeUnit.MILLISECONDS.toSeconds(this) % 60 16 | val minutes = TimeUnit.MILLISECONDS.toMinutes(this) % 60 17 | return when (val hours = TimeUnit.MILLISECONDS.toHours(this)) { 18 | 0L -> "${normalize(minutes)}:${normalize(seconds)}" 19 | else -> "${normalize(hours)}:${normalize(minutes)}:${normalize(seconds)}" 20 | } 21 | } 22 | 23 | val Int.formatAsFileSize: String 24 | get() = toLong().formatAsFileSize 25 | 26 | val Long.formatAsFileSize: String 27 | get() = log2(if (this != 0L) toDouble() else 1.0).toInt().div(10).let { 28 | val precision = when (it) { 29 | 0 -> 0; 1 -> 1; else -> 2 30 | } 31 | val prefix = arrayOf("", "K", "M", "G", "T", "P", "E", "Z", "Y") 32 | String.format("%.${precision}f ${prefix[it]}B", toDouble() / 2.0.pow(it * 10.0)) 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/ProjectUtil.kt: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.jbexternal.util 2 | 3 | import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectNotificationAware 4 | import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTracker 5 | import com.intellij.openapi.externalSystem.service.project.trusted.ExternalSystemTrustedProjectDialog 6 | import com.intellij.openapi.project.Project 7 | 8 | @Suppress("DEPRECATION") 9 | object ProjectUtil { 10 | fun refreshProject(project: Project) { 11 | val projectNotificationAware = ExternalSystemProjectNotificationAware.getInstance(project) 12 | val systemIds = projectNotificationAware.getSystemIds() 13 | if (ExternalSystemTrustedProjectDialog.confirmLoadingUntrustedProject(project, systemIds)) { 14 | val projectTracker = ExternalSystemProjectTracker.getInstance(project) 15 | projectTracker.scheduleProjectRefresh() 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/ScopeUiUtil.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. 2 | package bitlap.sbt.analyzer.jbexternal.util 3 | 4 | import java.awt.Component 5 | import javax.swing.JCheckBox 6 | import javax.swing.JLabel 7 | import javax.swing.JList 8 | import javax.swing.JPanel 9 | import javax.swing.ListCellRenderer 10 | import javax.swing.ListSelectionModel 11 | 12 | import com.intellij.ide.nls.NlsMessages 13 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerDependency.Scope 14 | import com.intellij.openapi.externalSystem.util.ExternalSystemBundle 15 | import com.intellij.openapi.observable.properties.GraphProperty 16 | import com.intellij.openapi.observable.properties.ObservableMutableProperty 17 | import com.intellij.openapi.observable.properties.PropertyGraph 18 | import com.intellij.openapi.observable.util.bind 19 | import com.intellij.openapi.observable.util.whenItemSelected 20 | import com.intellij.openapi.observable.util.whenMousePressed 21 | import com.intellij.openapi.ui.popup.JBPopup 22 | import com.intellij.openapi.ui.popup.JBPopupFactory 23 | import com.intellij.openapi.util.NlsSafe 24 | import com.intellij.ui.ListUtil 25 | import com.intellij.ui.components.DropDownLink 26 | import com.intellij.ui.components.JBList 27 | import com.intellij.util.ui.JBUI 28 | import com.intellij.util.ui.ThreeStateCheckBox 29 | 30 | @Suppress("DEPRECATION") 31 | internal class SearchScopeSelector(property: ObservableMutableProperty>) : JPanel() { 32 | init { 33 | val dropDownLink = SearchScopeDropDownLink(property).apply { 34 | border = JBUI.Borders.empty(BORDER, ICON_TEXT_GAP / 2, BORDER, BORDER) 35 | } 36 | val label = JLabel(ExternalSystemBundle.message("external.system.dependency.analyzer.scope.label")).apply { 37 | border = JBUI.Borders.empty(BORDER, BORDER, BORDER, ICON_TEXT_GAP / 2) 38 | }.apply { labelFor = dropDownLink } 39 | 40 | layout = com.intellij.ide.plugins.newui.HorizontalLayout(0) 41 | border = JBUI.Borders.empty() 42 | add(label) 43 | add(dropDownLink) 44 | } 45 | } 46 | 47 | private class SearchScopePopupContent(scopes: List) : JBList() { 48 | 49 | private val propertyGraph = PropertyGraph(isBlockPropagation = false) 50 | private val anyScopeProperty = propertyGraph.lazyProperty(::suggestAnyScopeState) 51 | private val scopeProperties = 52 | scopes.map { ScopeProperty.Just(it.scope, propertyGraph.lazyProperty { it.isSelected }) } 53 | 54 | private fun suggestAnyScopeState(): ThreeStateCheckBox.State { 55 | return when { 56 | scopeProperties.all { it.property.get() } -> ThreeStateCheckBox.State.SELECTED 57 | !scopeProperties.any { it.property.get() } -> ThreeStateCheckBox.State.NOT_SELECTED 58 | else -> ThreeStateCheckBox.State.DONT_CARE 59 | } 60 | } 61 | 62 | private fun suggestScopeState(currentState: Boolean): Boolean { 63 | return when (anyScopeProperty.get()) { 64 | ThreeStateCheckBox.State.SELECTED -> true 65 | ThreeStateCheckBox.State.NOT_SELECTED -> false 66 | ThreeStateCheckBox.State.DONT_CARE -> currentState 67 | } 68 | } 69 | 70 | fun afterChange(listener: (List) -> Unit) { 71 | for (scope in scopeProperties) { 72 | scope.property.afterChange { 73 | listener(scopeProperties.map { ScopeItem(it.scope, it.property.get()) }) 74 | } 75 | } 76 | } 77 | 78 | init { 79 | val anyScope = ScopeProperty.Any(anyScopeProperty) 80 | model = createDefaultListModel(listOf(anyScope) + scopeProperties) 81 | border = emptyListBorder() 82 | cellRenderer = SearchScopePropertyRenderer() 83 | selectionMode = ListSelectionModel.SINGLE_SELECTION 84 | ListUtil.installAutoSelectOnMouseMove(this) 85 | setupListPopupPreferredWidth(this) 86 | whenMousePressed { 87 | when (val scope = selectedValue) { 88 | is ScopeProperty.Any -> scope.property.set(ThreeStateCheckBox.nextState(scope.property.get(), false)) 89 | is ScopeProperty.Just -> scope.property.set(!scope.property.get()) 90 | } 91 | } 92 | 93 | propertyGraph.afterPropagation { 94 | repaint() 95 | } 96 | for (scope in scopeProperties) { 97 | anyScopeProperty.dependsOn(scope.property) { 98 | suggestAnyScopeState() 99 | } 100 | scope.property.dependsOn(anyScopeProperty) { 101 | suggestScopeState(scope.property.get()) 102 | } 103 | } 104 | } 105 | 106 | companion object { 107 | fun createPopup(scopes: List, onChange: (List) -> Unit): JBPopup { 108 | val content = SearchScopePopupContent(scopes) 109 | content.afterChange(onChange) 110 | return JBPopupFactory.getInstance().createComponentPopupBuilder(content, null).createPopup() 111 | } 112 | } 113 | } 114 | 115 | private class SearchScopePropertyRenderer : ListCellRenderer { 116 | override fun getListCellRendererComponent( 117 | list: JList, value: ScopeProperty, index: Int, isSelected: Boolean, cellHasFocus: Boolean 118 | ): Component { 119 | val checkBox = when (value) { 120 | is ScopeProperty.Any -> ThreeStateCheckBox(ExternalSystemBundle.message("external.system.dependency.analyzer.scope.any")).apply { 121 | isThirdStateEnabled = false 122 | }.apply { state = value.property.get() }.bind(value.property) 123 | 124 | is ScopeProperty.Just -> JCheckBox(value.scope.title).apply { this@apply.isSelected = value.property.get() } 125 | .bind(value.property) 126 | } 127 | return checkBox.apply { border = emptyListCellBorder(list, index, if (index > 0) 1 else 0) } 128 | .apply { background = if (isSelected) list.selectionBackground else list.background } 129 | .apply { foreground = if (isSelected) list.selectionForeground else list.foreground } 130 | .apply { isOpaque = true }.apply { isEnabled = list.isEnabled }.apply { font = list.font } 131 | } 132 | } 133 | 134 | private class SearchScopeDropDownLink( 135 | property: ObservableMutableProperty> 136 | ) : DropDownLink>( 137 | property.get(), 138 | { SearchScopePopupContent.createPopup(property.get(), it::selectedItem.setter) }) { 139 | override fun popupPoint() = super.popupPoint().apply { x += insets.left } 140 | 141 | override fun itemToString(item: List): @NlsSafe String { 142 | return when { 143 | item.all { it.isSelected } -> ExternalSystemBundle.message("external.system.dependency.analyzer.scope.any") 144 | !item.any { it.isSelected } -> ExternalSystemBundle.message("external.system.dependency.analyzer.scope.none") 145 | else -> { 146 | val scopes = item.filter { it.isSelected }.map { it.scope.title } 147 | abbreviate(NlsMessages.formatNarrowAndList(scopes), 30) 148 | } 149 | } 150 | } 151 | 152 | init { 153 | autoHideOnDisable = false 154 | foreground = JBUI.CurrentTheme.Label.foreground() 155 | whenItemSelected { text = itemToString(selectedItem) } 156 | bind(property) 157 | } 158 | } 159 | 160 | internal class ScopeItem( 161 | val scope: Scope, val isSelected: Boolean 162 | ) { 163 | override fun toString() = "$isSelected: $scope" 164 | } 165 | 166 | private sealed interface ScopeProperty { 167 | class Any(val property: GraphProperty) : ScopeProperty 168 | class Just(val scope: Scope, val property: GraphProperty) : ScopeProperty 169 | } -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.jbexternal.util 2 | 3 | /** 4 | * Copy from commons-lang3 5 | */ 6 | fun abbreviate(str: String, maxWidth: Int): String { 7 | return abbreviate(str, "...", 0, maxWidth) 8 | } 9 | 10 | fun abbreviate(str: String, abbrevMarker: String, maxWidth: Int): String { 11 | return abbreviate(str, abbrevMarker, 0, maxWidth) 12 | } 13 | 14 | fun isAnyEmpty(vararg css: CharSequence): Boolean { 15 | if (css.isEmpty()) { 16 | return false 17 | } else { 18 | val length = css.size 19 | for (var3 in 0 until length) { 20 | val cs = css[var3] 21 | if (cs.isEmpty()) { 22 | return true 23 | } 24 | } 25 | 26 | return false 27 | } 28 | } 29 | 30 | fun abbreviate(str: String, abbrevMarker: String, stringOffset: Int, maxWidth: Int): String { 31 | var offset = stringOffset 32 | return if (str.isNotEmpty() && "" == abbrevMarker && maxWidth > 0) { 33 | str.substring(0, maxWidth) 34 | } else if (isAnyEmpty(str, abbrevMarker)) { 35 | str 36 | } else { 37 | val abbrevMarkerLength = abbrevMarker.length 38 | val minAbbrevWidth = abbrevMarkerLength + 1 39 | val minAbbrevWidthOffset = abbrevMarkerLength + abbrevMarkerLength + 1 40 | if (maxWidth < minAbbrevWidth) { 41 | throw IllegalArgumentException(String.format("Minimum abbreviation width is %d", minAbbrevWidth)) 42 | } else { 43 | val strLen = str.length 44 | if (strLen <= maxWidth) { 45 | str 46 | } else { 47 | if (offset > strLen) { 48 | offset = strLen 49 | } 50 | if (strLen - offset < maxWidth - abbrevMarkerLength) { 51 | offset = strLen - (maxWidth - abbrevMarkerLength) 52 | } 53 | if (offset <= abbrevMarkerLength + 1) { 54 | str.substring(0, maxWidth - abbrevMarkerLength) + abbrevMarker 55 | } else if (maxWidth < minAbbrevWidthOffset) { 56 | throw IllegalArgumentException( 57 | String.format( 58 | "Minimum abbreviation width with offset is %d", minAbbrevWidthOffset 59 | ) 60 | ) 61 | } else { 62 | if (offset + maxWidth - abbrevMarkerLength < strLen) abbrevMarker + abbreviate( 63 | str.substring(offset), abbrevMarker, maxWidth - abbrevMarkerLength 64 | ) else abbrevMarker + str.substring(strLen - (maxWidth - abbrevMarkerLength)) 65 | } 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/UiUtils.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. 2 | package bitlap.sbt.analyzer.jbexternal.util 3 | 4 | import java.awt.BorderLayout 5 | import javax.swing.JComponent 6 | import javax.swing.JLabel 7 | import javax.swing.JList 8 | import javax.swing.JPanel 9 | import javax.swing.JTree 10 | import javax.swing.Icon 11 | import javax.swing.border.Border 12 | 13 | import bitlap.sbt.analyzer.jbexternal.DependencyAnalyzerManager 14 | 15 | import com.intellij.icons.AllIcons 16 | import com.intellij.openapi.Disposable 17 | import com.intellij.openapi.actionSystem.ActionToolbar 18 | import com.intellij.openapi.actionSystem.ActionUpdateThread 19 | import com.intellij.openapi.actionSystem.AnAction 20 | import com.intellij.openapi.actionSystem.AnActionEvent 21 | import com.intellij.openapi.actionSystem.DefaultActionGroup 22 | import com.intellij.openapi.actionSystem.ToggleAction 23 | import com.intellij.openapi.actionSystem.impl.ActionButton 24 | import com.intellij.openapi.externalSystem.util.ExternalSystemBundle 25 | import com.intellij.openapi.observable.properties.ObservableBooleanProperty 26 | import com.intellij.openapi.observable.properties.ObservableMutableProperty 27 | import com.intellij.openapi.observable.properties.ObservableProperty 28 | import com.intellij.openapi.observable.util.bind 29 | import com.intellij.openapi.project.DumbAware 30 | import com.intellij.openapi.ui.SimpleToolWindowPanel 31 | import com.intellij.openapi.util.Disposer 32 | import com.intellij.openapi.util.IconLoader 33 | import com.intellij.ui.CardLayoutPanel 34 | import com.intellij.ui.OnePixelSplitter 35 | import com.intellij.ui.components.JBLoadingPanel 36 | import com.intellij.util.ui.JBUI 37 | import com.intellij.util.ui.components.BorderLayoutPanel 38 | import com.intellij.util.ui.tree.TreeUtil 39 | 40 | internal val PROJECT_ICON: Icon = 41 | IconLoader.getIcon("/icons/sbt_dependency_analyzer.svg", DependencyAnalyzerManager::class.java) 42 | internal const val BORDER = 6 43 | internal const val INDENT = 16 44 | internal const val ICON_TEXT_GAP = 4 45 | internal const val ACTION_BORDER = 2 46 | 47 | // import com.intellij.openapi.observable.util.whenDisposed 48 | fun Disposable.whenDisposed(listener: () -> Unit) { 49 | Disposer.register(this, Disposable { listener() }) 50 | } 51 | 52 | internal fun emptyListBorder(): Border { 53 | return JBUI.Borders.empty() 54 | } 55 | 56 | internal fun emptyListCellBorder(list: JList<*>, index: Int, indent: Int = 0): Border { 57 | val topGap = if (index > 0) BORDER / 2 else BORDER 58 | val bottomGap = if (index < list.model.size - 1) BORDER / 2 else BORDER 59 | val leftGap = BORDER + INDENT * indent 60 | val rightGap = BORDER 61 | return JBUI.Borders.empty(topGap, leftGap, bottomGap, rightGap) 62 | } 63 | 64 | internal fun setupListPopupPreferredWidth(list: JList<*>) { 65 | list.setPreferredWidth(maxOf(JBUI.scale(164), list.preferredSize.width)) 66 | } 67 | 68 | internal fun JComponent.setPreferredWidth(width: Int) { 69 | preferredSize = preferredSize.also { it.width = width } 70 | } 71 | 72 | internal fun label(text: String) = JLabel(text).apply { border = JBUI.Borders.empty(BORDER) } 73 | 74 | internal fun label(property: ObservableProperty) = label(property.get()).bind(property) 75 | 76 | internal fun toolWindowPanel(configure: SimpleToolWindowPanel.() -> Unit) = 77 | SimpleToolWindowPanel(true, true).apply { configure() } 78 | 79 | internal fun toolbarPanel(configure: BorderLayoutPanel.() -> Unit) = 80 | BorderLayoutPanel().apply { layout = BorderLayout() }.apply { border = JBUI.Borders.empty(1, 2) } 81 | .apply { withMinimumHeight(JBUI.scale(30)) }.apply { withPreferredHeight(JBUI.scale(30)) }.apply { configure() } 82 | 83 | @Suppress("DEPRECATION") 84 | internal fun horizontalPanel(vararg components: JComponent) = 85 | JPanel().apply { layout = com.intellij.ide.plugins.newui.HorizontalLayout(0) } 86 | .apply { border = JBUI.Borders.empty() }.apply { components.forEach(::add) } 87 | 88 | internal fun horizontalSplitPanel(proportionKey: String, proportion: Float, configure: OnePixelSplitter.() -> Unit) = 89 | OnePixelSplitter(false, proportionKey, proportion).apply { configure() } 90 | 91 | internal fun cardPanel(createPanel: (T) -> JComponent) = object : CardLayoutPanel() { 92 | override fun prepare(key: T) = key 93 | override fun create(ui: T) = createPanel(ui) 94 | } 95 | 96 | internal fun > C.bind(property: ObservableProperty): C = apply { 97 | select(property.get(), true) 98 | property.afterChange { select(it, true) } 99 | } 100 | 101 | internal fun C.bind(property: ObservableBooleanProperty): C = apply { 102 | if (property.get()) { 103 | startLoading() 104 | } else { 105 | stopLoading() 106 | } 107 | property.afterSet { startLoading() } 108 | property.afterReset { stopLoading() } 109 | } 110 | 111 | internal fun toggleAction(property: ObservableMutableProperty): ToggleAction = 112 | object : ToggleAction(), DumbAware { 113 | override fun isSelected(e: AnActionEvent) = property.get() 114 | override fun setSelected(e: AnActionEvent, state: Boolean) = property.set(state) 115 | override fun getActionUpdateThread() = ActionUpdateThread.EDT 116 | } 117 | 118 | internal fun action(action: (AnActionEvent) -> Unit): AnAction = object : AnAction(), DumbAware { 119 | override fun actionPerformed(e: AnActionEvent) = action(e) 120 | } 121 | 122 | internal fun popupActionGroup(vararg actions: AnAction) = DefaultActionGroup(*actions).apply { isPopup = true } 123 | 124 | internal fun AnAction.asActionButton(place: String) = 125 | ActionButton(this, templatePresentation.clone(), place, ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE).apply { 126 | border = JBUI.Borders.empty(ACTION_BORDER) 127 | } 128 | 129 | internal fun separator() = JLabel(AllIcons.General.Divider).apply { border = JBUI.Borders.empty(ACTION_BORDER) } 130 | .apply { font = JBUI.Fonts.toolbarSmallComboBoxFont() } 131 | 132 | internal fun expandTreeAction(tree: JTree) = action { TreeUtil.expandAll(tree) }.apply { 133 | templatePresentation.text = ExternalSystemBundle.message("external.system.dependency.analyzer.resolved.tree.expand") 134 | }.apply { templatePresentation.icon = AllIcons.Actions.Expandall } 135 | 136 | internal fun collapseTreeAction(tree: JTree) = action { TreeUtil.collapseAll(tree, 0) }.apply { 137 | templatePresentation.text = 138 | ExternalSystemBundle.message("external.system.dependency.analyzer.resolved.tree.collapse") 139 | }.apply { templatePresentation.icon = AllIcons.Actions.Collapseall } 140 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/sbt_dependency_analyzer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/messages/SbtDependencyAnalyzerBundle.properties: -------------------------------------------------------------------------------- 1 | analyzer.task.error.unknown.text=Unknown problem occurs "{0}/{1}/{2}", please report the problem to the developer 2 | analyzer.task.error.text=Unable to parse the file "{0}", cause by "{1}", please report the problem to the developer 3 | analyzer.task.error.title=Unable to use Sbt Dependency Analyzer plugin 4 | analyzer.action.name=Analyze sbt dependencies... 5 | analyzer.action.whatsNew.text=What''s New in {0} 6 | analyzer.action.gotoAction.text=Goto dependency 7 | analyzer.action.excludeAction.text=Exclude dependency 8 | analyzer.action.excludeAction.description=// Exclude selected dependencies from user-defined dependencies or delete user-defined dependencies (Experimental) 9 | analyzer.notification.gotoSdap=Goto "{0}" 10 | analyzer.notification.addSdap.title=Sbt Dependency Analyzer is ready 11 | analyzer.notification.addSdap.text=The "{0}" has been added or updated, \ 12 | and it has also been ignored by the ".git/info/exclude" file in the git environment, \ 13 | please wait a moment, go to see more. 14 | analyzer.notification.setting.changed.title=Sbt Dependency Analyzer settings have been changed 15 | analyzer.notification.reimport.title=Sbt Dependency Analyzer is refreshing the project 16 | analyzer.notification.updated.failure.title=Failed to load! 17 | analyzer.notification.updated.failure.text=Open in browser↗ 18 | analyzer.notification.updated.title={0} plugin updated to v{1} 19 | analyzer.notification.updated.text=Thank you for downloading \ 20 | Sbt Dependency Analyzer! \ 21 |
Change notes (releases notes)\:
\ 22 |
{1}
23 | analyzer.notification.updated.gotoBrowser=Go to see↗ 24 | analyzer.refresh.dependencies.text=Refresh dependencies 25 | analyzer.refresh.dependencies.description=Refresh dependencies using reimport 26 | analyzer.refresh.snapshot.dependencies.text=Refresh snapshot dependencies 27 | analyzer.refresh.snapshot.dependencies.description=Set the "csrConfiguration" to refresh snapshot dependencies 28 | analyzer.settings.page.name=Sbt Dependency Analyzer 29 | analyzer.packagesearch.dependency.sbt.possible.places.to.add.new.dependency=Possible places to add new dependency 30 | analyzer.packagesearch.dependency.sbt.could.not.generate.expression.string.to.add=// Could not generate expression string to add 31 | analyzer.packagesearch.dependency.sbt.select.a.place.from.the.list.above.to.enable.this.preview=// Select a place from the list above to enable this preview 32 | analyzer.notification.dependency.excluded.title=Excluded dependency "{0}" successfully 33 | analyzer.notification.dependency.excluded.failed.title=Excluded dependency "{0}" failed 34 | analyzer.notification.dependency.removed.title=Removed dependency "{0}" successfully 35 | analyzer.notification.dependency.removed.failed.title=Removed dependency "{0}" failed 36 | analyzer.notification.ok=Ok -------------------------------------------------------------------------------- /src/main/resources/messages/SbtDependencyAnalyzerBundle_zh.properties: -------------------------------------------------------------------------------- 1 | analyzer.task.error.unknown.text=出现未知问题 “{0}/{1}/{2}”, 请向开发人员报告问题 2 | analyzer.task.error.title=无法使用 Sbt Dependency Analyzer 插件 3 | analyzer.task.error.text=无法分析文件 “{0}”,原因 “{1}”, 请向开发人员报告问题 4 | analyzer.action.name=分析 sbt 依赖... 5 | analyzer.action.whatsNew.text={0} 最新功能 6 | analyzer.action.gotoAction.text=查看依赖 7 | analyzer.action.excludeAction.text=排除依赖 8 | analyzer.action.excludeAction.description=// 从用户定义依赖中排除选中的依赖或删除用户定义依赖 (实验性) 9 | analyzer.notification.gotoSdap=打开 “{0}” 10 | analyzer.notification.addSdap.title=Sbt Dependency Analyzer 已就绪 11 | analyzer.notification.addSdap.text=已添加或更新文件 “{0}”,\ 12 | 并且在git环境中也已经被“.git/info/exclude”文件忽略了,\ 13 | 请稍等片刻,前往查看更多 14 | analyzer.notification.setting.changed.title=Sbt Dependency Analyzer 配置已被更改 15 | analyzer.notification.reimport.title=Sbt Dependency Analyzer 正在刷新项目 16 | analyzer.notification.updated.failure.title=加载失败! 17 | analyzer.notification.updated.failure.text=在浏览器中打开↗ 18 | analyzer.notification.updated.title={0} 插件已更新为 v{1} 19 | analyzer.notification.updated.text=感谢下载 \ 20 | Sbt Dependency Analyzer! \ 21 |
更新说明 (releases notes)\:
\ 22 |
{1}
23 | analyzer.notification.updated.gotoBrowser=去看看↗ 24 | analyzer.refresh.dependencies.text=刷新依赖 25 | analyzer.refresh.dependencies.description=重新导入以刷新依赖 26 | analyzer.refresh.snapshot.dependencies.text=刷新快照依赖 27 | analyzer.refresh.snapshot.dependencies.description=设置 “csrConfiguration” 以刷新快照依赖 28 | analyzer.settings.page.name=Sbt 依赖分析 29 | analyzer.packagesearch.dependency.sbt.could.not.generate.expression.string.to.add=// 无法生成要添加的表达式字符串 30 | analyzer.packagesearch.dependency.sbt.possible.places.to.add.new.dependency=要添加新依赖项的可能位置 31 | analyzer.packagesearch.dependency.sbt.select.a.place.from.the.list.above.to.enable.this.preview=// 从上面的列表中选择一个位置以启用此预览 32 | analyzer.notification.dependency.excluded.title=排除依赖 “{0}” 成功 33 | analyzer.notification.dependency.excluded.failed.title=排除依赖 “{0}” 失败 34 | analyzer.notification.dependency.removed.title=删除依赖 “{0}” 成功 35 | analyzer.notification.dependency.removed.failed.title=删除依赖 “{0}” 失败 36 | analyzer.notification.ok=好的 -------------------------------------------------------------------------------- /src/main/resources/messages/SbtPluginExternalBundle.properties: -------------------------------------------------------------------------------- 1 | analyzer.external.showSize.name=Show Size -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/Constants.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer 2 | 3 | import scala.concurrent.duration.* 4 | 5 | object Constants: 6 | 7 | final val SEPARATOR: String = "/" 8 | final val LINE_SEPARATOR: String = "\n" 9 | 10 | final val ARTIFACT_SEPARATOR: String = ":" 11 | final val EMPTY_STRING: String = "" 12 | 13 | final val SINGLE_SBT_MODULE = "__SINGLE_MODULE__" 14 | final val ROOT_SBT_MODULE = "__ROOT_MODULE__" 15 | 16 | final val PROJECT = "project" 17 | 18 | final val TIMEOUT = 10.minutes 19 | 20 | final val INTERVAL_TIMEOUT = 1010.milliseconds 21 | 22 | final val CHANGE_NOTES_SEPARATOR = "" 23 | 24 | end Constants 25 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/DependencyScopeEnum.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer 2 | 3 | enum DependencyScopeEnum: 4 | // see https://github.com/JetBrains/intellij-scala/blob/idea232.x/sbt/sbt-impl/src/org/jetbrains/sbt/language/utils/SbtDependencyCommon.scala 5 | case Compile, Provided, Test 6 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/SbtDependencyAnalyzerBundle.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer 2 | 3 | import java.util.* 4 | 5 | import org.jetbrains.annotations.Nls 6 | import org.jetbrains.annotations.NotNull 7 | import org.jetbrains.annotations.PropertyKey 8 | 9 | import com.intellij.* 10 | import com.intellij.openapi.diagnostic.Logger 11 | import com.intellij.openapi.util.registry.Registry 12 | 13 | import SbtDependencyAnalyzerBundle.* 14 | 15 | final class SbtDependencyAnalyzerBundle(private val pathToBundle: String) extends AbstractBundle(pathToBundle): 16 | private val adaptedControl = ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES) 17 | 18 | private lazy val adaptedBundle: AbstractBundle = { 19 | val dynamicLocale = getDynamicLocale 20 | if dynamicLocale != null then 21 | if (dynamicLocale.toLanguageTag == Locale.ENGLISH.toLanguageTag) { 22 | new AbstractBundle(pathToBundle) { 23 | override def findBundle( 24 | pathToBundle: String, 25 | loader: ClassLoader, 26 | control: ResourceBundle.Control 27 | ): ResourceBundle = { 28 | val dynamicBundle = ResourceBundle.getBundle(pathToBundle, dynamicLocale, loader, adaptedControl) 29 | if dynamicBundle == null then super.findBundle(pathToBundle, loader, control) else dynamicBundle 30 | } 31 | } 32 | } else null 33 | else null 34 | } 35 | 36 | def getAdaptedMessage(@PropertyKey(resourceBundle = BUNDLE) key: String, params: Any*): String = { 37 | if (adaptedBundle != null) adaptedBundle.getMessage(key, params*) else getMessage(key, params*) 38 | } 39 | 40 | override def findBundle( 41 | pathToBundle: String, 42 | loader: ClassLoader, 43 | control: ResourceBundle.Control 44 | ): ResourceBundle = 45 | val dynamicLocale = getDynamicLocale 46 | if dynamicLocale != null then 47 | if forceFollowLanguagePack then ResourceBundle.getBundle(pathToBundle, dynamicLocale, loader, adaptedControl) 48 | else ResourceBundle.getBundle(pathToBundle, dynamicLocale, loader, control) 49 | else super.findBundle(pathToBundle, loader, control) 50 | 51 | end findBundle 52 | 53 | end SbtDependencyAnalyzerBundle 54 | 55 | object SbtDependencyAnalyzerBundle: 56 | private val LOG = Logger.getInstance(classOf[SbtDependencyAnalyzerBundle]) 57 | 58 | private lazy val forceFollowLanguagePack: Boolean = { 59 | Registry.get("bitlap.sbt.analyzer.SbtDependencyAnalyzerBundle").asBoolean() 60 | } 61 | 62 | private lazy val getDynamicLocale: Locale = { 63 | try { 64 | DynamicBundle.getLocale 65 | } catch { 66 | case _: NoSuchMethodError => 67 | LOG.debug("NoSuchMethodError: DynamicBundle.getLocale()") 68 | null 69 | } 70 | } 71 | 72 | final val BUNDLE = "messages.SbtDependencyAnalyzerBundle" 73 | 74 | final val INSTANCE = new SbtDependencyAnalyzerBundle(BUNDLE) 75 | 76 | @Nls def message(@NotNull @PropertyKey(resourceBundle = BUNDLE) key: String, @NotNull params: AnyRef*): String = 77 | INSTANCE.getAdaptedMessage(key, params*) 78 | 79 | end SbtDependencyAnalyzerBundle 80 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/SbtDependencyAnalyzerConfigurable.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer 2 | 3 | import javax.swing.JComponent 4 | 5 | import com.intellij.openapi.options.* 6 | import com.intellij.openapi.project.Project 7 | 8 | final class SbtDependencyAnalyzerConfigurable(project: Project) extends SearchableConfigurable.Parent.Abstract { 9 | 10 | private lazy val panel: SbtDependencyAnalyzerPanel = new SbtDependencyAnalyzerPanel(project) 11 | 12 | override def getId: String = SbtDependencyAnalyzerPlugin.PLUGIN_ID 13 | 14 | override def getDisplayName: String = SbtDependencyAnalyzerBundle.message("analyzer.settings.page.name") 15 | 16 | override def getHelpTopic: String = "default" 17 | 18 | override def createComponent(): JComponent = panel.$$$getRootComponent$$$() 19 | 20 | override def isModified: Boolean = panel.isModified 21 | 22 | override def apply(): Unit = panel.apply() 23 | 24 | override def reset(): Unit = panel.from() 25 | 26 | override def disposeUIResources(): Unit = {} 27 | 28 | override def buildConfigurables(): Array[Configurable] = Array() 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/SbtDependencyAnalyzerExtension.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer 2 | 3 | import org.jetbrains.sbt.project.SbtProjectSystem 4 | 5 | import com.intellij.openapi.Disposable 6 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerContributor 7 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerExtension 8 | import com.intellij.openapi.externalSystem.model.ProjectSystemId 9 | import com.intellij.openapi.project.Project 10 | 11 | final class SbtDependencyAnalyzerExtension extends DependencyAnalyzerExtension: 12 | 13 | override def isApplicable(systemId: ProjectSystemId): Boolean = 14 | systemId == SbtProjectSystem.Id 15 | 16 | override def createContributor(project: Project, parentDisposable: Disposable): DependencyAnalyzerContributor = 17 | SbtDependencyAnalyzerContributor(project) 18 | 19 | end SbtDependencyAnalyzerExtension 20 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/SbtDependencyAnalyzerIcons.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer 2 | 3 | import javax.swing.Icon 4 | 5 | import com.intellij.openapi.util.IconLoader 6 | 7 | /** icons 8 | */ 9 | object SbtDependencyAnalyzerIcons: 10 | 11 | val ICON: Icon = IconLoader.getIcon("/icons/sbt_dependency_analyzer.svg", SbtDependencyAnalyzerIcons.getClass) 12 | 13 | end SbtDependencyAnalyzerIcons 14 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/SbtDependencyAnalyzerPanel.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/SbtDependencyAnalyzerPanel.java: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer; 2 | 3 | import bitlap.sbt.analyzer.util.Notifications$; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.uiDesigner.core.Spacer; 6 | import com.jgoodies.forms.layout.CellConstraints; 7 | import com.jgoodies.forms.layout.FormLayout; 8 | 9 | import javax.swing.*; 10 | import java.awt.*; 11 | 12 | /** 13 | * TODO create panel by kotlin DSL 14 | */ 15 | @SuppressWarnings("unchecked") 16 | public class SbtDependencyAnalyzerPanel { 17 | public JPanel mainPanel; 18 | 19 | public JTextField organization; 20 | public JCheckBox compileCheckBox; 21 | public JCheckBox providedCheckBox; 22 | public JCheckBox testCheckBox; 23 | public JTextField fileCache; 24 | private final SettingsState settings; 25 | private final Project project; 26 | 27 | public SbtDependencyAnalyzerPanel(Project project) { 28 | this.project = project; 29 | this.settings = SettingsState.getSettings(project); 30 | } 31 | 32 | boolean isModified() { 33 | boolean disableAnalyzeCompile = settings.getDisableAnalyzeCompile() == compileCheckBox.isSelected(); 34 | boolean disableAnalyzeTest = settings.getDisableAnalyzeTest() == testCheckBox.isSelected(); 35 | boolean disableAnalyzeProvided = settings.getDisableAnalyzeProvided() == providedCheckBox.isSelected(); 36 | boolean fileCacheTimeout = String.valueOf(settings.fileCacheTimeout()).equals(fileCache.getText()); 37 | boolean org = settings.getOrganization() == null && organization.getText() == null || 38 | (organization.getText() != null && organization.getText().equals(settings.getOrganization())); 39 | 40 | return !(org && fileCacheTimeout && disableAnalyzeCompile && disableAnalyzeTest && disableAnalyzeProvided); 41 | 42 | } 43 | 44 | void apply() { 45 | // if data change, we publish a notification 46 | boolean changed = false; 47 | if (isModified()) { 48 | changed = true; 49 | } 50 | 51 | settings.setOrganization(organization.getText()); 52 | settings.setDisableAnalyzeCompile(compileCheckBox.isSelected()); 53 | settings.setDisableAnalyzeTest(testCheckBox.isSelected()); 54 | settings.setDisableAnalyzeProvided(providedCheckBox.isSelected()); 55 | try { 56 | int t = Integer.parseInt(fileCache.getText()); 57 | settings.setFileCacheTimeout(t); 58 | } catch (Exception ignore) { 59 | settings.setFileCacheTimeout(3600); 60 | } 61 | if (changed) { 62 | Notifications$.MODULE$.notifySettingsChanged(project); 63 | SettingsState.SettingsChangePublisher().onConfigurationChanged(this.project, settings); 64 | } 65 | } 66 | 67 | void from() { 68 | compileCheckBox.setSelected(settings.disableAnalyzeCompile()); 69 | providedCheckBox.setSelected(settings.disableAnalyzeProvided()); 70 | testCheckBox.setSelected(settings.disableAnalyzeTest()); 71 | organization.setText(settings.getOrganization()); 72 | fileCache.setText(String.valueOf(settings.fileCacheTimeout())); 73 | } 74 | 75 | 76 | { 77 | // GUI initializer generated by IntelliJ IDEA GUI Designer 78 | // >>> IMPORTANT!! <<< 79 | // DO NOT EDIT OR ADD ANY CODE HERE! 80 | $$$setupUI$$$(); 81 | } 82 | 83 | /** 84 | * Method generated by IntelliJ IDEA GUI Designer 85 | * >>> IMPORTANT!! <<< 86 | * DO NOT edit this method OR call it in your code! 87 | * 88 | * @noinspection ALL 89 | */ 90 | private void $$$setupUI$$$() { 91 | mainPanel = new JPanel(); 92 | mainPanel.setLayout(new FormLayout("fill:d:grow,fill:d:grow,fill:d:grow,fill:d:grow,fill:d:grow", "center:max(d;4px):noGrow,center:32px:noGrow,center:max(d;4px):noGrow,center:12px:grow")); 93 | mainPanel.setAlignmentX(0.0f); 94 | mainPanel.setAlignmentY(0.0f); 95 | mainPanel.setAutoscrolls(false); 96 | mainPanel.setMaximumSize(new Dimension(400, 120)); 97 | mainPanel.setMinimumSize(new Dimension(-1, -1)); 98 | mainPanel.setPreferredSize(new Dimension(350, 120)); 99 | final JLabel label1 = new JLabel(); 100 | label1.setMaximumSize(new Dimension(80, 30)); 101 | label1.setMinimumSize(new Dimension(-1, -1)); 102 | label1.setPreferredSize(new Dimension(80, 30)); 103 | label1.setText("Disable Scope:"); 104 | CellConstraints cc = new CellConstraints(); 105 | mainPanel.add(label1, cc.xy(1, 3)); 106 | final JLabel label2 = new JLabel(); 107 | label2.setMaximumSize(new Dimension(80, 30)); 108 | label2.setMinimumSize(new Dimension(-1, -1)); 109 | label2.setPreferredSize(new Dimension(80, 30)); 110 | label2.setText("Organization:"); 111 | mainPanel.add(label2, cc.xy(1, 2)); 112 | testCheckBox = new JCheckBox(); 113 | testCheckBox.setMaximumSize(new Dimension(60, 25)); 114 | testCheckBox.setMinimumSize(new Dimension(-1, -1)); 115 | testCheckBox.setPreferredSize(new Dimension(60, 25)); 116 | testCheckBox.setText("Test"); 117 | mainPanel.add(testCheckBox, cc.xy(2, 3)); 118 | compileCheckBox = new JCheckBox(); 119 | compileCheckBox.setMaximumSize(new Dimension(60, 25)); 120 | compileCheckBox.setMinimumSize(new Dimension(-1, -1)); 121 | compileCheckBox.setPreferredSize(new Dimension(60, 25)); 122 | compileCheckBox.setText("Compile"); 123 | mainPanel.add(compileCheckBox, cc.xy(3, 3)); 124 | providedCheckBox = new JCheckBox(); 125 | providedCheckBox.setMaximumSize(new Dimension(60, 25)); 126 | providedCheckBox.setMinimumSize(new Dimension(-1, -1)); 127 | providedCheckBox.setPreferredSize(new Dimension(60, 25)); 128 | providedCheckBox.setText("Provided"); 129 | mainPanel.add(providedCheckBox, cc.xy(4, 3)); 130 | final JLabel label3 = new JLabel(); 131 | label3.setMaximumSize(new Dimension(80, 30)); 132 | label3.setMinimumSize(new Dimension(-1, -1)); 133 | label3.setPreferredSize(new Dimension(80, 30)); 134 | label3.setText("File Cache Timeout:"); 135 | mainPanel.add(label3, cc.xy(1, 1)); 136 | fileCache = new JTextField(); 137 | fileCache.setColumns(8); 138 | fileCache.setHorizontalAlignment(2); 139 | fileCache.setMaximumSize(new Dimension(112, 30)); 140 | fileCache.setMinimumSize(new Dimension(-1, -1)); 141 | fileCache.setPreferredSize(new Dimension(112, 30)); 142 | fileCache.setText("3600"); 143 | mainPanel.add(fileCache, cc.xy(2, 1, CellConstraints.FILL, CellConstraints.DEFAULT)); 144 | organization = new JTextField(); 145 | organization.setColumns(8); 146 | organization.setHorizontalAlignment(2); 147 | organization.setMaximumSize(new Dimension(112, 30)); 148 | organization.setMinimumSize(new Dimension(-1, -1)); 149 | organization.setPreferredSize(new Dimension(112, 30)); 150 | organization.setText(""); 151 | mainPanel.add(organization, cc.xy(2, 2)); 152 | final JLabel label4 = new JLabel(); 153 | label4.setMaximumSize(new Dimension(60, 30)); 154 | label4.setMinimumSize(new Dimension(-1, -1)); 155 | label4.setPreferredSize(new Dimension(60, 30)); 156 | label4.setText("seconds"); 157 | mainPanel.add(label4, cc.xy(3, 1, CellConstraints.FILL, CellConstraints.DEFAULT)); 158 | final Spacer spacer1 = new Spacer(); 159 | mainPanel.add(spacer1, cc.xyw(1, 4, 4, CellConstraints.CENTER, CellConstraints.FILL)); 160 | final Spacer spacer2 = new Spacer(); 161 | mainPanel.add(spacer2, cc.xywh(5, 1, 1, 4, CellConstraints.FILL, CellConstraints.DEFAULT)); 162 | } 163 | 164 | /** 165 | * @noinspection ALL 166 | */ 167 | public JComponent $$$getRootComponent$$$() { 168 | return mainPanel; 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/SbtDependencyAnalyzerPlugin.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer 2 | 3 | import com.intellij.ide.plugins.IdeaPluginDescriptor 4 | import com.intellij.ide.plugins.PluginManagerCore 5 | import com.intellij.openapi.extensions.PluginId 6 | 7 | object SbtDependencyAnalyzerPlugin { 8 | 9 | val PLUGIN_ID = "org.bitlap.sbtDependencyAnalyzer" 10 | 11 | val descriptor: IdeaPluginDescriptor = PluginManagerCore.getPlugin(PluginId.getId(PLUGIN_ID)) 12 | 13 | lazy val version: String = descriptor.getVersion 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/SettingsState.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | 5 | import java.util.{ Collections, Map as JMap } 6 | 7 | import scala.beans.BeanProperty 8 | 9 | import com.intellij.openapi.application.ApplicationManager 10 | import com.intellij.openapi.components.* 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.util.messages.Topic 13 | import com.intellij.util.xmlb.XmlSerializerUtil 14 | 15 | @State(name = "SbtDependencyAnalyzer.Settings", storages = Array(new Storage("bitlap.sbt.dependency.analyzer.xml"))) 16 | final class SettingsState extends PersistentStateComponent[SettingsState] { 17 | 18 | @BeanProperty 19 | var sbtModules: JMap[String, String] = Collections.emptyMap() 20 | 21 | @BeanProperty 22 | var disableAnalyzeCompile: Boolean = false 23 | 24 | @BeanProperty 25 | var disableAnalyzeProvided: Boolean = false 26 | 27 | @BeanProperty 28 | var disableAnalyzeTest: Boolean = false 29 | 30 | @BeanProperty 31 | var organization: String = "" 32 | 33 | @BeanProperty 34 | var fileCacheTimeout: Int = 3600 35 | 36 | override def getState: SettingsState = this 37 | 38 | override def loadState(state: SettingsState): Unit = { 39 | XmlSerializerUtil.copyBean(state, this) 40 | } 41 | 42 | } 43 | 44 | object SettingsState { 45 | 46 | def getSettings(project: Project): SettingsState = project.getService(classOf[SettingsState]) 47 | 48 | val _Topic: Topic[SettingsChangeListener] = 49 | Topic.create("SbtDependencyAnalyzerSettingsChanged", classOf[SettingsChangeListener]) 50 | 51 | trait SettingsChangeListener: 52 | 53 | def onConfigurationChanged(project: Project, settingsState: SettingsState): Unit 54 | 55 | end SettingsChangeListener 56 | 57 | /** * 58 | * {{{ 59 | * ApplicationManager 60 | * .getApplication() 61 | * .messageBus 62 | * .connect(this) 63 | * .subscribe(SettingsChangeListener.TOPIC, this) 64 | * }}} 65 | */ 66 | val SettingsChangePublisher: SettingsChangeListener = 67 | ApplicationManager.getApplication.getMessageBus.syncPublisher(_Topic) 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/action/BaseRefreshDependenciesAction.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.action 2 | 3 | import java.util 4 | 5 | import com.intellij.openapi.actionSystem.{ ActionUpdateThread, AnActionEvent } 6 | import com.intellij.openapi.application.ApplicationManager 7 | import com.intellij.openapi.externalSystem.ExternalSystemManager 8 | import com.intellij.openapi.externalSystem.model.* 9 | import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType 10 | import com.intellij.openapi.externalSystem.service.internal.ExternalSystemProcessingManager 11 | import com.intellij.openapi.project.* 12 | 13 | abstract class BaseRefreshDependenciesAction extends DumbAwareAction() { 14 | 15 | lazy val eventText: String 16 | lazy val eventDescription: String 17 | 18 | override def getActionUpdateThread: ActionUpdateThread = ActionUpdateThread.BGT 19 | 20 | protected def getSystemIds(e: AnActionEvent): util.ArrayList[ProjectSystemId] = { 21 | val systemIds = new util.ArrayList[ProjectSystemId] 22 | val externalSystemId = e.getData(ExternalSystemDataKeys.EXTERNAL_SYSTEM_ID) 23 | if (externalSystemId == null) 24 | ExternalSystemManager.EP_NAME.forEachExtensionSafe((manager: ExternalSystemManager[?, ?, ?, ?, ?]) => 25 | systemIds.add(manager.getSystemId) 26 | ) 27 | else systemIds.add(externalSystemId) 28 | systemIds 29 | } 30 | 31 | override def update(e: AnActionEvent): Unit = { 32 | val project: Project = e.getProject 33 | if (project == null) { 34 | e.getPresentation.setEnabled(false) 35 | return 36 | } 37 | val systemIds: util.List[ProjectSystemId] = getSystemIds(e) 38 | if (systemIds.isEmpty) { 39 | e.getPresentation.setEnabled(false) 40 | return 41 | } 42 | e.getPresentation.setText(eventText) 43 | e.getPresentation.setDescription(eventDescription) 44 | val processingManager: ExternalSystemProcessingManager = 45 | ApplicationManager.getApplication.getService(classOf[ExternalSystemProcessingManager]) 46 | e.getPresentation.setEnabled( 47 | !processingManager.hasTaskOfTypeInProgress(ExternalSystemTaskType.RESOLVE_PROJECT, project) 48 | ) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/action/SbtDependencyAnalyzerAction.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.action 2 | 3 | import scala.jdk.CollectionConverters.* 4 | 5 | import bitlap.sbt.analyzer.* 6 | import bitlap.sbt.analyzer.jbexternal.* 7 | import bitlap.sbt.analyzer.util.SbtUtils 8 | 9 | import org.jetbrains.sbt.project.SbtProjectSystem 10 | 11 | import com.intellij.openapi.actionSystem.* 12 | import com.intellij.openapi.externalSystem.dependency.analyzer.* 13 | import com.intellij.openapi.externalSystem.model.* 14 | import com.intellij.openapi.externalSystem.model.project.* 15 | import com.intellij.openapi.externalSystem.model.project.dependencies.* 16 | import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil 17 | import com.intellij.openapi.externalSystem.view.* 18 | import com.intellij.openapi.module.Module 19 | 20 | final class ViewDependencyAnalyzerAction extends AbstractSbtDependencyAnalyzerAction[ExternalSystemNode[?]]: 21 | 22 | getTemplatePresentation.setText(SbtDependencyAnalyzerBundle.message("analyzer.action.name")) 23 | getTemplatePresentation.setIcon(SbtDependencyAnalyzerIcons.ICON) 24 | 25 | override def getDependencyScope(anActionEvent: AnActionEvent, selectedData: ExternalSystemNode[?]): String = 26 | val node = selectedData.findDependencyNode(classOf[DependencyScopeNode]) 27 | if (node == null) return null 28 | node.getScope 29 | end getDependencyScope 30 | 31 | override def getModule(anActionEvent: AnActionEvent, selectedData: ExternalSystemNode[?]): Module = 32 | val project = anActionEvent.getProject 33 | if (project == null) return null 34 | val node = selectedData.findNode(classOf[ModuleNode]) 35 | if (node == null) return null 36 | val data = node.getData 37 | if (data != null) return findModule(project, data) 38 | 39 | val projectNode = selectedData.findNode(classOf[ProjectNode]) 40 | if (projectNode == null) return null 41 | val projectData = projectNode.getData 42 | if (projectNode == null) return null 43 | findModule(project, projectData) 44 | end getModule 45 | 46 | override def getSelectedData(anActionEvent: AnActionEvent): ExternalSystemNode[?] = 47 | val module = anActionEvent.getData(ExternalSystemDataKeys.SELECTED_NODES) 48 | if (module == null) return null 49 | module.asScala.headOption.orNull 50 | end getSelectedData 51 | 52 | override def getSystemId(anActionEvent: AnActionEvent): ProjectSystemId = SbtProjectSystem.Id 53 | 54 | override def getDependencyData( 55 | anActionEvent: AnActionEvent, 56 | selectedData: ExternalSystemNode[?] 57 | ): DependencyAnalyzerDependency.Data = 58 | selectedData.getData match 59 | case pd: ProjectData => DAModule(pd.getInternalName) 60 | case md: ModuleData => DAModule(md.getModuleName) 61 | case _ => 62 | selectedData.getDependencyNode match 63 | case pdn: ProjectDependencyNode => DAModule(pdn.getProjectName) 64 | case adn: ArtifactDependencyNode => 65 | val size = SbtUtils.getLibrarySize(selectedData.getProject, adn.getDisplayName) 66 | val total = 67 | SbtUtils.getLibraryTotalSize(selectedData.getProject, adn.getDependencies.asScala.toList) 68 | SbtDAArtifact(adn.getGroup, adn.getModule, adn.getVersion, size, size + total) 69 | 70 | end getDependencyData 71 | 72 | end ViewDependencyAnalyzerAction 73 | 74 | final class ProjectViewDependencyAnalyzerAction extends AbstractSbtDependencyAnalyzerAction[Module]: 75 | 76 | getTemplatePresentation.setText(SbtDependencyAnalyzerBundle.message("analyzer.action.name")) 77 | getTemplatePresentation.setIcon(SbtDependencyAnalyzerIcons.ICON) 78 | 79 | override def getDependencyScope(anActionEvent: AnActionEvent, data: Module): String = null 80 | 81 | override def getModule(anActionEvent: AnActionEvent, selectedData: Module): Module = selectedData 82 | 83 | override def getSelectedData(anActionEvent: AnActionEvent): Module = 84 | val module = anActionEvent.getData(PlatformCoreDataKeys.MODULE) 85 | if (module == null) return null 86 | if (ExternalSystemApiUtil.isExternalSystemAwareModule(SbtProjectSystem.Id, module)) { 87 | module 88 | } else null 89 | end getSelectedData 90 | 91 | override def getSystemId(anActionEvent: AnActionEvent): ProjectSystemId = SbtProjectSystem.Id 92 | 93 | override def getDependencyData( 94 | anActionEvent: AnActionEvent, 95 | selectedData: Module 96 | ): DependencyAnalyzerDependency.Data = DAModule(selectedData.getName) 97 | 98 | override def isEnabledAndVisible(e: AnActionEvent): Boolean = { 99 | super.isEnabledAndVisible(e) 100 | && (e.getData(LangDataKeys.MODULE_CONTEXT_ARRAY) != null || !e.isFromContextMenu) 101 | } 102 | 103 | end ProjectViewDependencyAnalyzerAction 104 | 105 | final class ToolbarDependencyAnalyzerAction extends BaseDependencyAnalyzerAction(): 106 | 107 | getTemplatePresentation.setText(SbtDependencyAnalyzerBundle.message("analyzer.action.name")) 108 | getTemplatePresentation.setIcon(SbtDependencyAnalyzerIcons.ICON) 109 | 110 | private val viewAction = ViewDependencyAnalyzerAction() 111 | 112 | override def getSystemId(anActionEvent: AnActionEvent): ProjectSystemId = SbtProjectSystem.Id 113 | 114 | override def isEnabledAndVisible(anActionEvent: AnActionEvent): Boolean = true 115 | 116 | override def setSelectedState(dependencyAnalyzerView: DependencyAnalyzerView, anActionEvent: AnActionEvent): Unit = 117 | viewAction.setSelectedState(dependencyAnalyzerView, anActionEvent) 118 | 119 | end ToolbarDependencyAnalyzerAction 120 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/action/SbtDependencyAnalyzerActionUtil.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.action 2 | 3 | import scala.jdk.CollectionConverters.* 4 | 5 | import bitlap.sbt.analyzer.* 6 | import bitlap.sbt.analyzer.util.DependencyUtils 7 | 8 | import com.intellij.buildsystem.model.DeclaredDependency 9 | import com.intellij.buildsystem.model.unified.UnifiedCoordinates 10 | import com.intellij.openapi.actionSystem.* 11 | import com.intellij.openapi.externalSystem.dependency.analyzer.* 12 | import com.intellij.openapi.module.Module as OpenapiModule 13 | 14 | final case class ModifiableDependency( 15 | module: OpenapiModule, 16 | coordinates: UnifiedCoordinates, 17 | declaredDependency: Option[DeclaredDependency], 18 | candidateDeclaredDependencies: List[DeclaredDependency], 19 | parentDependency: DependencyAnalyzerDependency 20 | ) 21 | 22 | object SbtDependencyAnalyzerActionUtil { 23 | 24 | def getModifiableDependency(e: AnActionEvent): ModifiableDependency = 25 | val project = e.getProject 26 | val dependency = e.getData(DependencyAnalyzerView.Companion.getDEPENDENCY) 27 | if (project == null || dependency == null) return null 28 | 29 | val coordinates: UnifiedCoordinates = getUnifiedCoordinates(dependency) 30 | val parentDependencyAndModule = getParentModule(project, dependency) 31 | if (coordinates == null || parentDependencyAndModule == null) return null 32 | 33 | val (parentDependency, module) = parentDependencyAndModule 34 | 35 | val candidateDeclaredDependencies = DependencyUtils 36 | .getDeclaredDependency(module) 37 | val declared = candidateDeclaredDependencies.find(dc => 38 | // hard code, see SbtDependencyUtils#getLibraryDependenciesOrPlacesFromPsi 39 | val artifactName = 40 | if ( 41 | coordinates.getArtifactId.endsWith("_3") || coordinates.getArtifactId.endsWith("_2.13") || 42 | coordinates.getArtifactId.endsWith("_2.12") || coordinates.getArtifactId.endsWith("_2.11") 43 | ) coordinates.getArtifactId.split('_').head 44 | else coordinates.getArtifactId 45 | (dc.getCoordinates.getArtifactId == coordinates.getArtifactId || 46 | dc.getCoordinates.getArtifactId == artifactName || 47 | // maybe a fixed artifact 48 | dc.getCoordinates.getVersion == artifactName) && 49 | dc.getCoordinates.getGroupId == coordinates.getGroupId 50 | ) 51 | 52 | ModifiableDependency(module, coordinates, declared, candidateDeclaredDependencies, parentDependency) 53 | end getModifiableDependency 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/action/SbtDependencyAnalyzerExcludeAction.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.action 2 | 3 | import bitlap.sbt.analyzer.* 4 | import bitlap.sbt.analyzer.model.AnalyzerCommandNotFoundException 5 | import bitlap.sbt.analyzer.util.* 6 | import bitlap.sbt.analyzer.util.packagesearch.* 7 | 8 | import com.intellij.buildsystem.model.unified.{ UnifiedCoordinates, UnifiedDependency } 9 | import com.intellij.openapi.actionSystem.* 10 | import com.intellij.openapi.diagnostic.Logger 11 | 12 | final class SbtDependencyAnalyzerExcludeAction extends BaseRefreshDependenciesAction: 13 | 14 | override lazy val eventText: String = SbtDependencyAnalyzerBundle.message("analyzer.action.excludeAction.text") 15 | 16 | override lazy val eventDescription: String = 17 | SbtDependencyAnalyzerBundle.message("analyzer.action.excludeAction.description") 18 | private val LOG = Logger.getInstance(classOf[SbtDependencyAnalyzerExcludeAction]) 19 | 20 | override def actionPerformed(e: AnActionEvent): Unit = { 21 | Option(SbtDependencyAnalyzerActionUtil.getModifiableDependency(e)).foreach { modifiableDependency => 22 | val parent = getUnifiedCoordinates(modifiableDependency.parentDependency) 23 | if (modifiableDependency.parentDependency.getParent != null) { 24 | val unifiedDependency = 25 | new UnifiedDependency(parent, modifiableDependency.parentDependency.getParent.getScope.getTitle) 26 | val coordinates: UnifiedCoordinates = modifiableDependency.coordinates 27 | if (coordinates == parent) { 28 | try { 29 | // remove declared dependency 30 | SbtDependencyModifier.removeDependency(modifiableDependency.module, unifiedDependency) 31 | Notifications.notifyDependencyChanged( 32 | modifiableDependency.module.getProject, 33 | coordinates.getDisplayName, 34 | self = true 35 | ) 36 | } catch { 37 | case e: Exception => 38 | LOG.error(s"Cannot remove declared dependency: ${coordinates.getDisplayName}", e) 39 | Notifications.notifyDependencyChanged( 40 | modifiableDependency.module.getProject, 41 | coordinates.getDisplayName, 42 | self = true, 43 | success = false 44 | ) 45 | } 46 | 47 | } else { 48 | // add exclude coordinates 49 | val ret = 50 | SbtDependencyModifier.addExcludeToDependency(modifiableDependency.module, unifiedDependency, coordinates) 51 | if (ret) { 52 | Notifications.notifyDependencyChanged( 53 | modifiableDependency.module.getProject, 54 | coordinates.getDisplayName, 55 | success = true, 56 | self = false 57 | ) 58 | } else { 59 | Notifications.notifyDependencyChanged( 60 | modifiableDependency.module.getProject, 61 | coordinates.getDisplayName, 62 | success = false, 63 | self = false 64 | ) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | end SbtDependencyAnalyzerExcludeAction 72 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/action/SbtDependencyAnalyzerGoToAction.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package action 5 | 6 | import scala.util.Try 7 | 8 | import bitlap.sbt.analyzer.* 9 | 10 | import org.jetbrains.sbt.project.SbtProjectSystem 11 | 12 | import com.intellij.ide.util.PsiNavigationSupport 13 | import com.intellij.openapi.actionSystem.* 14 | import com.intellij.openapi.diagnostic.Logger 15 | import com.intellij.openapi.externalSystem.dependency.analyzer.* 16 | import com.intellij.pom.Navigatable 17 | import com.intellij.psi.PsiElement 18 | 19 | final class SbtDependencyAnalyzerGoToAction extends DependencyAnalyzerGoToAction(SbtProjectSystem.Id): 20 | 21 | getTemplatePresentation.setText( 22 | SbtDependencyAnalyzerBundle.message("analyzer.action.gotoAction.text") 23 | ) 24 | 25 | private val LOG = Logger.getInstance(classOf[SbtDependencyAnalyzerGoToAction]) 26 | 27 | // PsiNavigationSupport 28 | override def getNavigatable(e: AnActionEvent): Navigatable = 29 | Option(SbtDependencyAnalyzerActionUtil.getModifiableDependency(e)) 30 | .flatMap(_.declaredDependency) 31 | .flatMap { dependency => 32 | Try { 33 | // warn: this will always yield false since type com.intellij.psi.PsiElement and class Tuple3 are unrelated 34 | // add asInstanceOf to fix it 35 | val data = dependency.getDataContext.getData(CommonDataKeys.PSI_ELEMENT).asInstanceOf[AnyRef] 36 | data match 37 | case t: (_, _, _) => 38 | t._1 match 39 | case element: PsiElement => Some(element) 40 | case _ => None 41 | case _ => None 42 | }.getOrElse { 43 | LOG.error(s"Cannot get 'PSI_ELEMENT' as 'PsiElement' for ${dependency.getCoordinates}") 44 | None 45 | } 46 | } 47 | .map(psiElement => PsiNavigationSupport.getInstance().getDescriptor(psiElement)) 48 | .orNull 49 | end getNavigatable 50 | 51 | end SbtDependencyAnalyzerGoToAction 52 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/action/SbtDependencyAnalyzerOpenConfigAction.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package action 5 | 6 | import bitlap.sbt.analyzer.util.SbtUtils 7 | 8 | import org.jetbrains.sbt.project.SbtProjectSystem 9 | 10 | import com.intellij.openapi.actionSystem.AnActionEvent 11 | import com.intellij.openapi.externalSystem.dependency.analyzer.{ 12 | DependencyAnalyzerDependency as Dependency, 13 | DependencyAnalyzerView, 14 | ExternalSystemDependencyAnalyzerOpenConfigAction 15 | } 16 | import com.intellij.openapi.externalSystem.service.settings.ExternalSystemConfigLocator 17 | import com.intellij.openapi.vfs.LocalFileSystem 18 | import com.intellij.openapi.vfs.VirtualFile 19 | 20 | final class SbtDependencyAnalyzerOpenConfigAction 21 | extends ExternalSystemDependencyAnalyzerOpenConfigAction(SbtProjectSystem.Id): 22 | 23 | override def getConfigFile(e: AnActionEvent): VirtualFile = { 24 | val externalSystemConfigPath = getConfigFileOption(Option(getExternalProjectPath(e))) 25 | // if we cannot find module config, goto root config 26 | val configPath = externalSystemConfigPath 27 | .orElse(getConfigFileOption(SbtUtils.getExternalProjectPath(e.getProject).headOption)) 28 | .orNull 29 | if (configPath == null || configPath.isDirectory) null else configPath 30 | } 31 | 32 | private def getConfigFileOption(externalProjectPath: Option[String]): Option[VirtualFile] = { 33 | val fileSystem = LocalFileSystem.getInstance() 34 | val externalProjectDirectory = externalProjectPath.map(fileSystem.refreshAndFindFileByPath) 35 | val locator = ExternalSystemConfigLocator.EP_NAME.findFirstSafe(_.getTargetExternalSystemId == SbtProjectSystem.Id) 36 | if (locator == null) { 37 | return null 38 | } 39 | 40 | val externalSystemConfigPath = externalProjectDirectory.toList.filterNot(_ == null).map(locator.adjust) 41 | 42 | externalSystemConfigPath.filterNot(_ == null).headOption 43 | } 44 | 45 | override def getExternalProjectPath(e: AnActionEvent): String = 46 | val dependency = e.getData(DependencyAnalyzerView.Companion.getDEPENDENCY) 47 | if (dependency == null) return null 48 | dependency.getData match 49 | case dm: Dependency.Data.Module => 50 | val moduleData = dm.getUserData(Module_Data) 51 | if (moduleData == null) null else moduleData.getLinkedExternalProjectPath 52 | case _ => null 53 | end getExternalProjectPath 54 | 55 | end SbtDependencyAnalyzerOpenConfigAction 56 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/action/SbtRefreshDependenciesAction.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package action 5 | 6 | import bitlap.sbt.analyzer.util.SbtReimportProject 7 | 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | 10 | final class SbtRefreshDependenciesAction extends BaseRefreshDependenciesAction: 11 | 12 | override lazy val eventText: String = SbtDependencyAnalyzerBundle.message("analyzer.refresh.dependencies.text") 13 | 14 | override lazy val eventDescription: String = 15 | SbtDependencyAnalyzerBundle.message("analyzer.refresh.dependencies.description") 16 | 17 | override def actionPerformed(e: AnActionEvent): Unit = { 18 | SbtReimportProject.ReimportProjectPublisher.onReimportProject(e.getProject) 19 | } 20 | 21 | end SbtRefreshDependenciesAction 22 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/action/SbtRefreshSnapshotDependenciesAction.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.action 2 | 3 | import bitlap.sbt.analyzer.* 4 | import bitlap.sbt.analyzer.task.* 5 | import bitlap.sbt.analyzer.util.SbtReimportProject 6 | import bitlap.sbt.analyzer.util.SbtUtils 7 | 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | 10 | final class SbtRefreshSnapshotDependenciesAction extends BaseRefreshDependenciesAction: 11 | 12 | override lazy val eventText: String = 13 | SbtDependencyAnalyzerBundle.message("analyzer.refresh.snapshot.dependencies.text") 14 | 15 | override lazy val eventDescription: String = 16 | SbtDependencyAnalyzerBundle.message("analyzer.refresh.snapshot.dependencies.description") 17 | 18 | override def actionPerformed(e: AnActionEvent): Unit = { 19 | SbtShellOutputAnalysisTask.refreshSnapshotsTask.executeCommand(e.getProject) 20 | SbtReimportProject.ReimportProjectPublisher.onReimportProject(e.getProject) 21 | } 22 | 23 | end SbtRefreshSnapshotDependenciesAction 24 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/activity/BaseProjectActivity.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.activity 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.startup.ProjectActivity 6 | 7 | import kotlin.coroutines.Continuation 8 | 9 | abstract class BaseProjectActivity(private val runOnlyOnce: Boolean = false) extends ProjectActivity { 10 | private var veryFirstProjectOpening: Boolean = true 11 | 12 | override def execute(project: Project, continuation: Continuation[? >: kotlin.Unit]): AnyRef = { 13 | if ( 14 | ApplicationManager.getApplication.isUnitTestMode || (runOnlyOnce && !veryFirstProjectOpening) || project.isDisposed 15 | ) { 16 | return continuation 17 | } 18 | // FIXME: should use continuation 19 | veryFirstProjectOpening = false 20 | if (onBeforeRunActivity(project)) { 21 | onRunActivity(project) 22 | } 23 | continuation 24 | } 25 | 26 | private def onBeforeRunActivity(project: Project): Boolean = true 27 | 28 | protected def onRunActivity(project: Project): Unit 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/activity/PluginUpdateActivity.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package activity 5 | 6 | import org.jetbrains.plugins.scala.project.Version 7 | 8 | import com.intellij.ide.plugins.IdeaPluginDescriptor 9 | import com.intellij.ide.util.PropertiesComponent 10 | import com.intellij.openapi.project.* 11 | import com.intellij.openapi.util.* 12 | import com.intellij.ui.* 13 | import com.intellij.util.ui.JBUI 14 | 15 | import analyzer.util.Notifications 16 | 17 | object PluginUpdateActivity: 18 | private val InitialVersion = "0.0.0" 19 | private lazy val VersionProperty = s"${SbtDependencyAnalyzerPlugin.PLUGIN_ID}.version" 20 | 21 | end PluginUpdateActivity 22 | 23 | final class PluginUpdateActivity extends BaseProjectActivity { 24 | 25 | import PluginUpdateActivity.* 26 | 27 | override def onRunActivity(project: Project): Unit = { 28 | checkUpdate(project) 29 | } 30 | 31 | private def checkUpdate(project: Project): Unit = { 32 | val plugin = SbtDependencyAnalyzerPlugin.descriptor 33 | val versionString = plugin.getVersion 34 | val properties = PropertiesComponent.getInstance() 35 | val lastVersionString = properties.getValue(VersionProperty, InitialVersion) 36 | if (versionString == lastVersionString) { 37 | return 38 | } 39 | 40 | val version = Version(versionString) 41 | val lastVersion = Version(lastVersionString) 42 | if (version == lastVersion) { 43 | return 44 | } 45 | 46 | // Simple handling of notifications 47 | val isNewVersion = version > lastVersion 48 | if (isNewVersion && showUpdateNotification(project, plugin, version)) { 49 | properties.setValue(VersionProperty, versionString) 50 | } 51 | } 52 | 53 | private def showUpdateNotification( 54 | project: Project, 55 | plugin: IdeaPluginDescriptor, 56 | version: Version 57 | ): Boolean = { 58 | val latestChangeNotes = 59 | if (plugin.getChangeNotes == null) "
" 60 | else plugin.getChangeNotes.split(Constants.CHANGE_NOTES_SEPARATOR)(0) 61 | val title = SbtDependencyAnalyzerBundle.message( 62 | "analyzer.notification.updated.title", 63 | plugin.getName, 64 | version.presentation 65 | ) 66 | val partStyle = s"margin-top: ${JBUI.scale(8)}px;" 67 | val content = SbtDependencyAnalyzerBundle.message( 68 | "analyzer.notification.updated.text", 69 | partStyle, 70 | latestChangeNotes, 71 | version.presentation 72 | ) 73 | Notifications.notifyUpdateActivity(project, version, title, content) 74 | true 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/activity/WhatsNew.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.activity 2 | 3 | import bitlap.sbt.analyzer.SbtDependencyAnalyzerBundle 4 | 5 | import org.jetbrains.plugins.scala.project.Version 6 | 7 | import com.intellij.ide.BrowserUtil 8 | import com.intellij.openapi.application.* 9 | import com.intellij.openapi.diagnostic.Logger 10 | import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.util.Conditions 13 | import com.intellij.ui.jcef.JBCefApp 14 | 15 | object WhatsNew: 16 | private lazy val Log = Logger.getInstance(getClass) 17 | private val ReleaseNotes = "https://github.com/bitlap/intellij-sbt-dependency-analyzer/releases/tag/v" 18 | 19 | def canBrowseInHTMLEditor: Boolean = JBCefApp.isSupported 20 | 21 | def getReleaseNotes(version: Version): String = ReleaseNotes + version.presentation 22 | 23 | def browse(version: Version, project: Project): Unit = { 24 | val url = ReleaseNotes + version.presentation 25 | if (project != null && canBrowseInHTMLEditor) { 26 | ApplicationManager.getApplication.invokeLater( 27 | () => { 28 | try { 29 | // TODO Why does it often fail to open? 30 | HTMLEditorProvider.openEditor( 31 | project, 32 | SbtDependencyAnalyzerBundle 33 | .message("analyzer.action.whatsNew.text", "Sbt Dependency Analyzer"), 34 | url, 35 | // language=HTML 36 | s"""
37 | |
${SbtDependencyAnalyzerBundle.message( 38 | "analyzer.notification.updated.failure.title" 39 | )}
40 | | 44 | |
""".stripMargin 45 | ) 46 | } catch { 47 | case e: Throwable => 48 | Log.warn("""Failed to load "What's New" page""", e) 49 | BrowserUtil.browse(url) 50 | } 51 | }, 52 | ModalityState.nonModal(), 53 | Conditions.is(project.getDisposed) 54 | ) 55 | } else { 56 | BrowserUtil.browse(url) 57 | } 58 | } 59 | 60 | end WhatsNew 61 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/activity/WhatsNewAction.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package activity 5 | 6 | import org.jetbrains.plugins.scala.project.Version 7 | 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.project.DumbAwareAction 10 | 11 | final class WhatsNewAction extends DumbAwareAction { 12 | 13 | getTemplatePresentation.setText( 14 | SbtDependencyAnalyzerBundle.message("analyzer.action.whatsNew.text", "Sbt Dependency Analyzer") 15 | ) 16 | 17 | override def actionPerformed(e: AnActionEvent): Unit = { 18 | WhatsNew.browse(Version(SbtDependencyAnalyzerPlugin.descriptor.getVersion), e.getProject) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/model/AnalyzerException.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.model 2 | 3 | import bitlap.sbt.analyzer.DependencyScopeEnum 4 | 5 | sealed abstract class AnalyzerException(msg: String) extends RuntimeException(msg) 6 | final case class AnalyzerCommandNotFoundException(msg: String) extends AnalyzerException(msg) 7 | 8 | final case class AnalyzerCommandUnknownException( 9 | command: String, 10 | moduleId: String, 11 | scope: DependencyScopeEnum, 12 | msg: String 13 | ) extends AnalyzerException(msg) 14 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/model/ArtifactInfo.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.model 2 | 3 | import bitlap.sbt.analyzer.Constants 4 | 5 | final case class ArtifactInfo(id: Int, group: String, artifact: String, version: String) { 6 | 7 | override def toString: String = { 8 | s"$group${Constants.ARTIFACT_SEPARATOR}$artifact${Constants.ARTIFACT_SEPARATOR}$version" 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/model/Dependencies.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.model 2 | 3 | final case class Dependencies(dependencies: List[ArtifactInfo], relations: List[Relation]) 4 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/model/ModuleContext.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.model 2 | 3 | import bitlap.sbt.analyzer.DependencyScopeEnum 4 | 5 | final case class ModuleContext( 6 | analysisFile: String, 7 | currentModuleId: String, 8 | scope: DependencyScopeEnum, 9 | organization: String, 10 | ideaModuleNamePaths: Map[String, String] = Map.empty, 11 | isScalaJs: Boolean = false, 12 | isScalaNative: Boolean = false, 13 | ideaModuleIdSbtModuleNames: Map[String, String] = Map.empty, // sbt module name == sbt artifact name 14 | isTest: Boolean = false 15 | ) 16 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/model/Relation.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.model 2 | 3 | final case class Relation(head: Int, tail: Int, label: String) 4 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/package.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer 2 | 3 | import scala.concurrent.ExecutionContext 4 | import scala.concurrent.duration.Duration 5 | import scala.jdk.CollectionConverters.* 6 | 7 | import bitlap.sbt.analyzer.parser.AnalyzedFileType 8 | 9 | import org.jetbrains.sbt.project.* 10 | 11 | import com.intellij.buildsystem.model.unified.UnifiedCoordinates 12 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerDependency 13 | import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerDependency.Data 14 | import com.intellij.openapi.externalSystem.model.project.* 15 | import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunnableState.* 16 | import com.intellij.openapi.externalSystem.service.project.IdeModelsProviderImpl 17 | import com.intellij.openapi.externalSystem.util.* 18 | import com.intellij.openapi.module.Module 19 | import com.intellij.openapi.project.Project 20 | import com.intellij.openapi.util.Key 21 | 22 | lazy val Module_Data: Key[ModuleData] = Key.create[ModuleData]("SbtDependencyAnalyzerContributor.ModuleData") 23 | 24 | given ExecutionContext = ExecutionContext.Implicits.global 25 | 26 | given AnalyzedFileType = AnalyzedFileType.Dot 27 | 28 | def getUnifiedCoordinates(dependency: DependencyAnalyzerDependency): UnifiedCoordinates = 29 | dependency.getData match { 30 | case data: DependencyAnalyzerDependency.Data.Artifact => getUnifiedCoordinates(data) 31 | case data: DependencyAnalyzerDependency.Data.Module => getUnifiedCoordinates(data) 32 | } 33 | 34 | def getUnifiedCoordinates(data: DependencyAnalyzerDependency.Data.Artifact): UnifiedCoordinates = 35 | UnifiedCoordinates(data.getGroupId, data.getArtifactId, data.getVersion) 36 | 37 | def getUnifiedCoordinates(data: DependencyAnalyzerDependency.Data.Module): UnifiedCoordinates = { 38 | val moduleData = data.getUserData(Module_Data) 39 | if (moduleData == null) return null 40 | UnifiedCoordinates(moduleData.getGroup, moduleData.getExternalName, moduleData.getVersion) 41 | } 42 | 43 | def getParentModule( 44 | project: Project, 45 | dependency: DependencyAnalyzerDependency 46 | ): (DependencyAnalyzerDependency, Module) = { 47 | val parentData = dependency.getParent 48 | if (parentData == null) return getRootModule(project, dependency, dependency) 49 | dependency.getParent.getData match 50 | case _: Data.Module => 51 | val data = dependency.getParent.getData.asInstanceOf[DependencyAnalyzerDependency.Data.Module] 52 | dependency -> getModule(project, data) 53 | case _ => getRootModule(project, dependency, dependency) 54 | } 55 | 56 | def getRootModule( 57 | project: Project, 58 | dependency: DependencyAnalyzerDependency, 59 | parent: DependencyAnalyzerDependency 60 | ): (DependencyAnalyzerDependency, Module) = { 61 | parent.getData match 62 | case _: Data.Module => 63 | val data = parent.getData.asInstanceOf[DependencyAnalyzerDependency.Data.Module] 64 | dependency -> getModule(project, data) 65 | case _ => getRootModule(project, parent, parent.getParent) 66 | } 67 | 68 | def getModule(project: Project, data: DependencyAnalyzerDependency.Data.Module): Module = { 69 | val moduleData: ModuleData = data.getUserData(Module_Data) 70 | if (moduleData == null) return null 71 | findModule(project, moduleData) 72 | } 73 | 74 | def findDependsModules(module: Module): List[Module] = { 75 | val modelsProvider = new IdeModelsProviderImpl(module.getProject) 76 | modelsProvider.getAllDependentModules(module).asScala.toList 77 | } 78 | 79 | def findModule(project: Project, moduleData: ModuleData): Module = { 80 | val modelsProvider = new IdeModelsProviderImpl(project) 81 | modelsProvider.findIdeModule(moduleData) 82 | } 83 | 84 | def findModule(project: Project, projectData: ProjectData): Module = 85 | findModule(project, projectData.getLinkedExternalProjectPath) 86 | 87 | def findModule(project: Project, projectPath: String): Module = { 88 | val moduleNode = ExternalSystemApiUtil.findModuleNode(project, SbtProjectSystem.Id, projectPath) 89 | if (moduleNode == null) return null 90 | findModule(project, moduleNode.getData) 91 | } 92 | 93 | def waitInterval(sleep: Duration = Constants.INTERVAL_TIMEOUT): Unit = { 94 | try { 95 | Thread.sleep(sleep.toMillis) 96 | } catch { 97 | case _: Throwable => 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/parser/AnalyzedDotFileParser.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package parser 5 | 6 | import scala.jdk.CollectionConverters.* 7 | 8 | import bitlap.sbt.analyzer.model.* 9 | import bitlap.sbt.analyzer.util.DependencyUtils 10 | import bitlap.sbt.analyzer.util.DependencyUtils.* 11 | 12 | import com.intellij.openapi.externalSystem.model.project.dependencies.* 13 | 14 | import guru.nidi.graphviz.model.{ Graph as _, * } 15 | 16 | /** Parse the dot file generated by task `dependencyDot`. dot file: sees 17 | * https://en.wikipedia.org/wiki/DOT_(graph_description_language) 18 | */ 19 | object AnalyzedDotFileParser: 20 | lazy val instance: AnalyzedFileParser = new AnalyzedDotFileParser 21 | end AnalyzedDotFileParser 22 | 23 | final class AnalyzedDotFileParser extends AnalyzedFileParser: 24 | 25 | import AnalyzedDotFileParser.* 26 | 27 | override val fileType: AnalyzedFileType = AnalyzedFileType.Dot 28 | 29 | /** transforming dependencies data into view data 30 | */ 31 | private def toDependencyNode(dep: ArtifactInfo): DependencyNode = { 32 | // module dependency 33 | val node = new ArtifactDependencyNodeImpl(dep.id.toLong, dep.group, dep.artifact, dep.version) 34 | node.setResolutionState(ResolutionState.RESOLVED) 35 | node 36 | } 37 | 38 | private def buildChildrenRelationsData( 39 | dependencies: Dependencies, 40 | depMap: Map[String, DependencyNode] 41 | ): (Map[String, String], Map[String, List[Int]]) = { 42 | val maxId = 43 | dependencies.dependencies.view 44 | .map(_.id) 45 | .sortWith((a, b) => a > b) 46 | .headOption 47 | .getOrElse(0) 48 | val graph = new Graph(maxId + 1) 49 | val relationLabelsMap = dependencies.relations.map { r => 50 | graph.addEdge(r.head, r.tail) 51 | s"${r.head}-${r.tail}" -> r.label 52 | }.toMap 53 | // find children all nodes,there may be indirect dependencies here. 54 | val parentChildrenMap = depMap.values.toSet.toSeq.map { topNode => 55 | val path = graph 56 | .dfs(topNode.getId.toInt) 57 | .tail 58 | .map(_.intValue()) 59 | .filter(childId => filterDirectChildren(topNode, childId, dependencies.relations)) 60 | topNode.getId.toString -> path.toList 61 | } 62 | (relationLabelsMap, parentChildrenMap.toMap) 63 | } 64 | 65 | /** build tree for dependency analyzer view 66 | */ 67 | override def buildDependencyTree(context: ModuleContext, root: DependencyScopeNode): DependencyScopeNode = { 68 | val data = getDependencyRelations(context) 69 | val dependencies: Dependencies = data.orNull 70 | val depMap = data.map(_.dependencies.map(a => a.id.toString -> toDependencyNode(a)).toMap).getOrElse(Map.empty) 71 | 72 | // if no relations for dependency object 73 | val (selfNode, otherNodes) = depMap.values.toSet.toSeq.partition(d => isSelfNode(d, context)) 74 | if (dependencies == null || dependencies.relations.isEmpty) { 75 | appendChildrenAndFixProjectNodes(root, otherNodes, context) 76 | return root 77 | } 78 | // build graph 79 | val (relationLabelsMap, parentChildrenMap) = buildChildrenRelationsData(dependencies, depMap) 80 | // get self 81 | // append children for self 82 | selfNode.foreach { node => 83 | buildChildrenNodes(node, parentChildrenMap, depMap, relationLabelsMap, context, dependencies.relations) 84 | } 85 | 86 | // transfer from self to root 87 | selfNode.foreach(d => root.getDependencies.addAll(d.getDependencies)) 88 | root 89 | } 90 | 91 | /** This is important to filter out non-direct dependencies 92 | */ 93 | private def filterDirectChildren(parent: DependencyNode, childId: Int, relations: List[Relation]) = { 94 | relations.exists(r => r.head == parent.getId && r.tail == childId) 95 | } 96 | 97 | /** Recursively create and add child nodes to root 98 | */ 99 | private def buildChildrenNodes( 100 | parentNode: DependencyNode, 101 | parentChildrenMap: Map[String, List[Int]], 102 | depMap: Map[String, DependencyNode], 103 | relationLabelsMap: Map[String, String], 104 | context: ModuleContext, 105 | relations: List[Relation] 106 | ): Unit = { 107 | val childIds = parentChildrenMap 108 | .getOrElse(parentNode.getId.toString, List.empty) 109 | .filter(cid => filterDirectChildren(parentNode, cid, relations)) 110 | if (childIds.isEmpty) return 111 | val childNodes = childIds.flatMap { id => 112 | depMap 113 | .get(id.toString) 114 | .map { 115 | case d @ (_: ArtifactDependencyNodeImpl) => 116 | val label = relationLabelsMap.getOrElse(s"${parentNode.getId}-$id", "") 117 | val newNode = new ArtifactDependencyNodeImpl(d.getId, d.getGroup, d.getModule, d.getVersion) 118 | if (label != null && label.nonEmpty) { 119 | newNode.setSelectionReason(label) 120 | } 121 | newNode.setResolutionState(d.getResolutionState) 122 | newNode 123 | case d => d 124 | } 125 | .toList 126 | } 127 | childNodes.foreach(d => buildChildrenNodes(d, parentChildrenMap, depMap, relationLabelsMap, context, relations)) 128 | appendChildrenAndFixProjectNodes(parentNode, childNodes, context) 129 | } 130 | 131 | /** parse dot file, get graph data 132 | */ 133 | private def getDependencyRelations(context: ModuleContext): Option[Dependencies] = 134 | val mutableGraph: MutableGraph = DotUtil.parseAsGraph(context) 135 | if (mutableGraph == null) None 136 | else 137 | val graphNodes: java.util.Collection[MutableNode] = mutableGraph.nodes() 138 | val links: java.util.Collection[Link] = mutableGraph.edges() 139 | 140 | val nodes = graphNodes.asScala.map { graphNode => 141 | graphNode.name().value() -> getArtifactInfoFromDisplayName(graphNode.name().value()) 142 | }.collect { case (name, Some(value)) => 143 | name -> value 144 | }.toMap 145 | 146 | val idMapping: Map[String, Int] = nodes.map(kv => kv._2.toString -> kv._2.id) 147 | 148 | val edges = links.asScala.map { l => 149 | val label = l.get("label").asInstanceOf[String] 150 | Relation( 151 | idMapping.getOrElse(l.from().name().value(), 0), 152 | idMapping.getOrElse(l.to().name().value(), 0), 153 | label 154 | ) 155 | } 156 | 157 | Some( 158 | Dependencies( 159 | nodes.values.toList, 160 | edges.toList 161 | ) 162 | ) 163 | 164 | end AnalyzedDotFileParser 165 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/parser/AnalyzedFileParser.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.parser 2 | 3 | import bitlap.sbt.analyzer.model.* 4 | 5 | import com.intellij.openapi.externalSystem.model.project.dependencies.* 6 | 7 | trait AnalyzedFileParser { 8 | 9 | val fileType: AnalyzedFileType 10 | 11 | def buildDependencyTree( 12 | context: ModuleContext, 13 | root: DependencyScopeNode 14 | ): DependencyScopeNode 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/parser/AnalyzedFileType.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.parser 2 | 3 | /** @param cmd 4 | * The sbt task to generate file to be analyzed 5 | * @param suffix 6 | * The suffix of the file generated by the [[cmd]] task 7 | */ 8 | enum AnalyzedFileType(val cmd: String, val suffix: String) { 9 | case Dot extends AnalyzedFileType("dependencyDot", "dot") 10 | case GraphML extends AnalyzedFileType("dependencyGraphML", "graphml") 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/parser/AnalyzedParserFactory.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.parser 2 | 3 | object AnalyzedParserFactory { 4 | 5 | def getInstance(builder: AnalyzedFileType): AnalyzedFileParser = { 6 | builder match 7 | case AnalyzedFileType.Dot => AnalyzedDotFileParser.instance 8 | // TODO 9 | case AnalyzedFileType.GraphML => throw new IllegalArgumentException("Parser type is not supported") 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/parser/DotUtil.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package parser 5 | 6 | import java.io.File 7 | import java.nio.file.Path 8 | 9 | import scala.util.Try 10 | import scala.util.control.Breaks 11 | import scala.util.control.Breaks.breakable 12 | 13 | import org.jetbrains.plugins.scala.extensions.inReadAction 14 | import org.jetbrains.plugins.scala.project.VirtualFileExt 15 | 16 | import com.intellij.openapi.diagnostic.Logger 17 | import com.intellij.openapi.vfs.VfsUtil 18 | 19 | import analyzer.util.Notifications 20 | import guru.nidi.graphviz.attribute.validate.ValidatorEngine 21 | import guru.nidi.graphviz.model.MutableGraph 22 | import guru.nidi.graphviz.parse.Parser 23 | import model.ModuleContext 24 | 25 | object DotUtil { 26 | 27 | private val LOG = Logger.getInstance(getClass) 28 | 29 | private lazy val parser = (new Parser).forEngine(ValidatorEngine.DOT).notValidating() 30 | 31 | private def parseAsGraphTestOnly(file: String): MutableGraph = { 32 | Try(parser.read(new File(file))).getOrElse(null) 33 | } 34 | 35 | def parseAsGraph(context: ModuleContext): MutableGraph = { 36 | if (context.isTest) return parseAsGraphTestOnly(context.analysisFile) 37 | val file = context.analysisFile 38 | try { 39 | var vfsFile = VfsUtil.findFile(Path.of(file), true) 40 | val start = System.currentTimeMillis() 41 | // TODO Tried all kinds of refreshes but nothing works. 42 | breakable { 43 | while (vfsFile == null) { 44 | vfsFile = VfsUtil.findFile(Path.of(file), true) 45 | if (vfsFile != null) { 46 | VfsUtil.markDirtyAndRefresh(true, true, true, vfsFile) 47 | Breaks.break() 48 | } else { 49 | if (System.currentTimeMillis() - start > Constants.TIMEOUT.toMillis) { 50 | Notifications.notifyParseFileError(file, "The file has expired") 51 | Breaks.break() 52 | } 53 | } 54 | } 55 | } 56 | inReadAction { 57 | if (vfsFile != null) { 58 | val f = vfsFile.findDocument.map(_.getImmutableCharSequence.toString).orNull 59 | parser.read(f) 60 | } else { 61 | Notifications.notifyParseFileError(file, "The file was not found") 62 | Breaks.break() 63 | } 64 | } 65 | } catch { 66 | case ignore: Throwable => 67 | LOG.error(ignore) 68 | Notifications.notifyParseFileError(file, "The file parsing failed") 69 | null 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/parser/Graph.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.parser; 2 | 3 | import scala.collection.mutable.ListBuffer 4 | 5 | final class Graph(size: Int) { 6 | 7 | private val graph = Array.fill(size)(ListBuffer[Int]()) 8 | 9 | def addEdge(v: Int, w: Int): Unit = { 10 | graph(v) += w 11 | } 12 | 13 | def dfs(v: Int): ListBuffer[Int] = { 14 | val visited = Array.fill(size + 1)(false) 15 | val res = ListBuffer[Int]() 16 | helper(v, visited, res) 17 | res 18 | } 19 | 20 | private def helper(v: Int, visited: Array[Boolean], res: ListBuffer[Int]): Unit = { 21 | visited(v) = true 22 | 23 | res += v 24 | 25 | graph(v).foreach(n => { 26 | if (!visited(n)) 27 | helper(n, visited, res) 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/task/DependencyDotTask.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package task 5 | 6 | import bitlap.sbt.analyzer.model.* 7 | import bitlap.sbt.analyzer.parser.* 8 | import bitlap.sbt.analyzer.util.DependencyUtils.* 9 | 10 | import org.jetbrains.plugins.scala.project.ModuleExt 11 | 12 | import com.intellij.openapi.externalSystem.model.project.ModuleData 13 | import com.intellij.openapi.externalSystem.model.project.dependencies.DependencyScopeNode 14 | import com.intellij.openapi.project.Project 15 | 16 | /** Process the `sbt dependencyDot` command, when the command execution is completed, use a callback to parse the file 17 | * content. 18 | */ 19 | final class DependencyDotTask extends SbtShellDependencyAnalysisTask: 20 | 21 | override val parserTypeEnum: AnalyzedFileType = AnalyzedFileType.Dot 22 | 23 | override def executeCommand( 24 | project: Project, 25 | moduleData: ModuleData, 26 | scope: DependencyScopeEnum, 27 | organization: String, 28 | moduleNamePaths: Map[String, String], 29 | ideaModuleIdSbtModules: Map[String, String] 30 | ): DependencyScopeNode = 31 | val module = findModule(project, moduleData) 32 | val moduleId = moduleData.getId.split(" ")(0) 33 | 34 | taskCompleteCallback(project, moduleData, scope) { file => 35 | val sbtModuleNameMap = 36 | if (ideaModuleIdSbtModules.isEmpty) Map(moduleId -> module.getName) 37 | else ideaModuleIdSbtModules 38 | AnalyzedParserFactory 39 | .getInstance(parserTypeEnum) 40 | .buildDependencyTree( 41 | ModuleContext( 42 | file, 43 | moduleId, 44 | scope, 45 | organization, 46 | moduleNamePaths, 47 | module.isScalaJs, 48 | module.isScalaNative, 49 | sbtModuleNameMap 50 | ), 51 | createRootScopeNode(scope, project) 52 | ) 53 | } 54 | end executeCommand 55 | 56 | end DependencyDotTask 57 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/task/ModuleNameTask.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package task 5 | 6 | import scala.collection.mutable 7 | 8 | import com.intellij.openapi.project.Project 9 | 10 | import Constants.* 11 | 12 | /** Process the `sbt moduleName` command, get all module names in sbt, it refers to the module name declared through 13 | * `name =: ` in `build.sbt` instead of Intellij IDEA. 14 | */ 15 | final class ModuleNameTask extends SbtShellOutputAnalysisTask[Map[String, String]]: 16 | import SbtShellOutputAnalysisTask.* 17 | 18 | /** {{{ 19 | * lazy val `rolls` = (project in file(".")) 20 | * .aggregate( 21 | * `rolls-compiler-plugin`, 22 | * `rolls-core`, 23 | * `rolls-csv`, 24 | * `rolls-zio`, 25 | * `rolls-plugin-tests`, 26 | * `rolls-docs` 27 | * ) 28 | * }}} 29 | * at present, if do not `aggregate` module rolls-docs, module rolls-docs cannot be analyzed. 30 | * 31 | * TODO fallback, exec cmd for single module: `rolls-docs / moduleName` to get module Name 32 | */ 33 | override def executeCommand(project: Project): Map[String, String] = 34 | val mms = getCommandOutputLines(project, "moduleName") 35 | val moduleIdSbtModuleNameMap = mutable.HashMap[String, String]() 36 | if ((mms.size & 1) == 0) { 37 | for (i <- 0 until mms.size - 1 by 2) { 38 | moduleIdSbtModuleNameMap.put(mms(i).trim, mms(i + 1).trim) 39 | } 40 | } else if (mms.size == 1) moduleIdSbtModuleNameMap.put(SINGLE_SBT_MODULE, mms.head) 41 | 42 | moduleIdSbtModuleNameMap.map { (k, v) => 43 | val key = k match 44 | case MODULE_NAME_INPUT_REGEX(_, _, moduleName, _, _) => moduleName.trim 45 | case ROOT_MODULE_NAME_INPUT_REGEX(_, _) => ROOT_SBT_MODULE 46 | case SINGLE_SBT_MODULE => SINGLE_SBT_MODULE 47 | case _ => EMPTY_STRING 48 | 49 | val value = v match 50 | case SHELL_OUTPUT_RESULT_REGEX(_, _, sbtModuleName) => sbtModuleName.trim 51 | case _ => EMPTY_STRING 52 | 53 | key -> value 54 | 55 | }.filter(kv => kv._1 != EMPTY_STRING && kv._2 != EMPTY_STRING).toMap 56 | 57 | end executeCommand 58 | 59 | end ModuleNameTask 60 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/task/OrganizationTask.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.task 2 | 3 | import com.intellij.openapi.project.Project 4 | 5 | /** Process the `sbt organization` command, get current project organization as artifact's groupId. 6 | */ 7 | final class OrganizationTask extends SbtShellOutputAnalysisTask[String]: 8 | 9 | import SbtShellOutputAnalysisTask.* 10 | 11 | override def executeCommand(project: Project): String = 12 | val outputLines = getCommandOutputLines(project, "organization") 13 | outputLines.lastOption.getOrElse("") match 14 | case SHELL_OUTPUT_RESULT_REGEX(_, _, org) => 15 | org.trim 16 | case _ => null 17 | 18 | end executeCommand 19 | 20 | end OrganizationTask 21 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/task/RefreshSnapshotsTask.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package task 5 | 6 | import org.jetbrains.plugins.scala.project.Version 7 | 8 | import com.intellij.openapi.project.Project 9 | 10 | import util.SbtUtils 11 | 12 | /** Process the `set csrConfiguration;update` command, load fresh snapshots for sbt shell. 13 | */ 14 | final class RefreshSnapshotsTask extends SbtShellOutputAnalysisTask[Unit]: 15 | 16 | override def executeCommand(project: Project): Unit = 17 | val sbtVersion = SbtUtils.getSbtVersion(project).binaryVersion 18 | // see https://www.scala-sbt.org/1.x/docs/Dependency-Management-Flow.html#Notes+on+SNAPSHOTs 19 | if (sbtVersion.major(2) >= Version("1.3")) { 20 | getCommandOutputLines( 21 | project, 22 | """ 23 | |set update / skip := false; 24 | |set csrConfiguration := csrConfiguration.value.withTtl(Option(scala.concurrent.duration.DurationInt(0).seconds)); 25 | |update; 26 | |""".stripMargin 27 | ) 28 | } else { 29 | getCommandOutputLines( 30 | project, 31 | """ 32 | |set update / skip := false; 33 | |update; 34 | |""".stripMargin 35 | ) 36 | } 37 | 38 | end RefreshSnapshotsTask 39 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/task/ReloadTask.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.task 2 | 3 | import com.intellij.openapi.project.Project 4 | 5 | /** Process the `sbt reload` command, load new setting for sbt shell. 6 | */ 7 | final class ReloadTask extends SbtShellOutputAnalysisTask[Unit]: 8 | 9 | override def executeCommand(project: Project): Unit = getCommandOutputLines(project, "reload") 10 | 11 | end ReloadTask 12 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/task/SbtShellDependencyAnalysisTask.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package task 5 | 6 | import scala.concurrent.* 7 | 8 | import org.jetbrains.sbt.shell.SbtShellCommunication 9 | 10 | import com.intellij.openapi.externalSystem.model.project.ModuleData 11 | import com.intellij.openapi.externalSystem.model.project.dependencies.DependencyScopeNode 12 | import com.intellij.openapi.project.Project 13 | 14 | import model.* 15 | import parser.* 16 | import util.DependencyUtils.* 17 | 18 | /** Tasks depend on the `addDependencyTreePlugin` plugin of the SBT. 19 | */ 20 | trait SbtShellDependencyAnalysisTask: 21 | 22 | val parserTypeEnum: AnalyzedFileType 23 | 24 | def executeCommand( 25 | project: Project, 26 | moduleData: ModuleData, 27 | scope: DependencyScopeEnum, 28 | organization: String, 29 | moduleNamePaths: Map[String, String], 30 | sbtModules: Map[String, String] 31 | ): DependencyScopeNode 32 | 33 | protected final def taskCompleteCallback( 34 | project: Project, 35 | moduleData: ModuleData, 36 | scope: DependencyScopeEnum 37 | )(buildNodeFunc: String => DependencyScopeNode): DependencyScopeNode = { 38 | val shellCommunication = SbtShellCommunication.forProject(project) 39 | val moduleId = moduleData.getId.split(" ")(0) 40 | val promise = Promise[Boolean]() 41 | val file = moduleData.getLinkedExternalProjectPath + analysisFilePath(scope, parserTypeEnum) 42 | val result = shellCommunication 43 | .command( 44 | getScopedCommandKey(moduleId, scope, parserTypeEnum.cmd), 45 | new StringBuilder(), 46 | SbtShellCommunication.listenerAggregator { 47 | case SbtShellCommunication.TaskComplete => 48 | if (!promise.isCompleted) { 49 | promise.success(true) 50 | } 51 | case SbtShellCommunication.ErrorWaitForInput => 52 | if (!promise.isCompleted) { 53 | promise.failure( 54 | AnalyzerCommandUnknownException( 55 | parserTypeEnum.cmd, 56 | moduleId, 57 | scope, 58 | SbtDependencyAnalyzerBundle.message("analyzer.task.error.title") 59 | ) 60 | ) 61 | } 62 | case SbtShellCommunication.Output(line) => 63 | if ( 64 | line.startsWith(SbtShellDependencyAnalysisTask.ERROR_PREFIX) && line 65 | .contains(parserTypeEnum.cmd) && !promise.isCompleted 66 | ) { 67 | promise.failure( 68 | AnalyzerCommandNotFoundException( 69 | SbtDependencyAnalyzerBundle.message("analyzer.task.error.title") 70 | ) 71 | ) 72 | } else if (line.startsWith(SbtShellDependencyAnalysisTask.ERROR_PREFIX) && !promise.isCompleted) { 73 | promise.failure( 74 | AnalyzerCommandUnknownException( 75 | parserTypeEnum.cmd, 76 | moduleId, 77 | scope, 78 | SbtDependencyAnalyzerBundle.message("analyzer.task.error.title") 79 | ) 80 | ) 81 | } 82 | case _ => 83 | 84 | } 85 | ) 86 | .flatMap(_ => promise.future) 87 | 88 | Await.result(result, Constants.TIMEOUT) 89 | buildNodeFunc(file) 90 | } 91 | 92 | end SbtShellDependencyAnalysisTask 93 | 94 | object SbtShellDependencyAnalysisTask: 95 | private val ERROR_PREFIX = "[error]" 96 | lazy val dependencyDotTask: SbtShellDependencyAnalysisTask = new DependencyDotTask 97 | 98 | end SbtShellDependencyAnalysisTask 99 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/task/SbtShellOutputAnalysisTask.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package task 5 | 6 | import scala.concurrent.* 7 | 8 | import org.jetbrains.sbt.shell.SbtShellCommunication 9 | 10 | import com.intellij.openapi.diagnostic.Logger 11 | import com.intellij.openapi.project.Project 12 | 13 | import Constants.* 14 | 15 | /** Tasks depend on the output of the SBT console. 16 | */ 17 | trait SbtShellOutputAnalysisTask[T]: 18 | private val LOG = Logger.getInstance(getClass) 19 | 20 | protected final def getCommandOutputLines(project: Project, command: String): List[String] = 21 | val shellCommunication = SbtShellCommunication.forProject(project) 22 | val executed: Future[String] = shellCommunication.command(command) 23 | val res = Await.result(executed, Constants.TIMEOUT) 24 | val result = res.split(Constants.LINE_SEPARATOR).toList.filter(_.startsWith("[info]")) 25 | if (result.isEmpty) { 26 | LOG.warn("Sbt Dependency Analyzer cannot find any output lines") 27 | } 28 | // see https://github.com/JetBrains/intellij-scala/blob/idea232.x/sbt/sbt-impl/src/org/jetbrains/sbt/shell/communication.scala 29 | // 1 second between multiple commands 30 | waitInterval() 31 | 32 | result 33 | end getCommandOutputLines 34 | 35 | def executeCommand(project: Project): T 36 | 37 | end SbtShellOutputAnalysisTask 38 | 39 | object SbtShellOutputAnalysisTask: 40 | 41 | // moduleName 42 | final val SHELL_OUTPUT_RESULT_REGEX = "(\\[info\\])(\\s|\\t)*(.*)".r 43 | final val MODULE_NAME_INPUT_REGEX = "(\\[info\\])(\\s|\\t)*(.*)(\\s|\\t)*/(\\s|\\t)*moduleName".r 44 | final val ROOT_MODULE_NAME_INPUT_REGEX = "(\\[info\\])(\\s|\\t)*moduleName".r 45 | 46 | lazy val sbtModuleNamesTask: SbtShellOutputAnalysisTask[Map[String, String]] = new ModuleNameTask 47 | 48 | lazy val organizationTask: SbtShellOutputAnalysisTask[String] = new OrganizationTask 49 | 50 | lazy val reloadTask: SbtShellOutputAnalysisTask[Unit] = new ReloadTask 51 | 52 | lazy val refreshSnapshotsTask: SbtShellOutputAnalysisTask[Unit] = new RefreshSnapshotsTask 53 | 54 | end SbtShellOutputAnalysisTask 55 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/util/Notifications.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package util 5 | 6 | import java.nio.file.Path 7 | 8 | import scala.concurrent.duration.* 9 | 10 | import bitlap.sbt.analyzer.activity.WhatsNew 11 | import bitlap.sbt.analyzer.activity.WhatsNew.canBrowseInHTMLEditor 12 | 13 | import org.jetbrains.plugins.scala.* 14 | import org.jetbrains.plugins.scala.extensions.* 15 | import org.jetbrains.plugins.scala.project.Version 16 | 17 | import com.intellij.icons.AllIcons 18 | import com.intellij.ide.BrowserUtil 19 | import com.intellij.notification.* 20 | import com.intellij.openapi.actionSystem.AnActionEvent 21 | import com.intellij.openapi.application.ApplicationManager 22 | import com.intellij.openapi.editor.Document 23 | import com.intellij.openapi.fileEditor.* 24 | import com.intellij.openapi.project.{ DumbAwareAction, Project } 25 | import com.intellij.openapi.vfs.{ VfsUtil, VirtualFile } 26 | 27 | /** SbtDependencyAnalyzer global notifier 28 | */ 29 | object Notifications { 30 | 31 | private lazy val NotificationGroup = 32 | NotificationGroupManager.getInstance().getNotificationGroup("Sbt.DependencyAnalyzer.Notification") 33 | 34 | private def getSdapText(project: Project): String = { 35 | val sbtVersion = SbtUtils.getSbtVersion(project).binaryVersion 36 | val line = if (sbtVersion.major(2) >= Version("1.4")) { 37 | "addDependencyTreePlugin" 38 | } else { 39 | if (sbtVersion.major(3) >= Version("0.13.10")) { 40 | "addSbtPlugin(\"net.virtual-void\" % \"sbt-dependency-graph\" % \"0.9.2\")" 41 | } else { 42 | "addSbtPlugin(\"net.virtual-void\" % \"sbt-dependency-graph\" % \"0.8.2\")" 43 | } 44 | } 45 | "// -- This file was mechanically generated by Sbt Dependency Analyzer Plugin: Do not edit! -- //" + Constants.LINE_SEPARATOR 46 | + line + Constants.LINE_SEPARATOR 47 | } 48 | 49 | def notifyParseFileError(file: String, msg: String): Unit = { 50 | // add notification when get vfsFile timeout 51 | val notification = NotificationGroup 52 | .createNotification( 53 | SbtDependencyAnalyzerBundle.message("analyzer.task.error.title"), 54 | SbtDependencyAnalyzerBundle.message("analyzer.task.error.text", file, msg), 55 | NotificationType.ERROR 56 | ) 57 | .setIcon(SbtDependencyAnalyzerIcons.ICON) 58 | .setImportant(true) 59 | notification.notify(null) 60 | } 61 | 62 | def notifySettingsChanged(project: Project): Unit = { 63 | val notification = NotificationGroup 64 | .createNotification( 65 | SbtDependencyAnalyzerBundle.message("analyzer.notification.setting.changed.title"), 66 | NotificationType.INFORMATION 67 | ) 68 | .setIcon(SbtDependencyAnalyzerIcons.ICON) 69 | notification.notify(project) 70 | } 71 | 72 | def notifyDependencyChanged( 73 | project: Project, 74 | dependency: String, 75 | success: Boolean = true, 76 | self: Boolean = false 77 | ): Unit = { 78 | val msg = 79 | if (!self) { 80 | if (success) SbtDependencyAnalyzerBundle.message("analyzer.notification.dependency.excluded.title", dependency) 81 | else SbtDependencyAnalyzerBundle.message("analyzer.notification.dependency.excluded.failed.title", dependency) 82 | } else { 83 | if (success) 84 | SbtDependencyAnalyzerBundle.message("analyzer.notification.dependency.removed.title", dependency) 85 | else SbtDependencyAnalyzerBundle.message("analyzer.notification.dependency.removed.failed.title", dependency) 86 | } 87 | NotificationGroup 88 | .createNotification(msg, NotificationType.INFORMATION) 89 | .setIcon(SbtDependencyAnalyzerIcons.ICON) 90 | .addAction( 91 | new NotificationAction( 92 | SbtDependencyAnalyzerBundle.message("analyzer.notification.ok") 93 | ) { 94 | override def actionPerformed(e: AnActionEvent, notification: Notification): Unit = { 95 | inReadAction { 96 | notification.expire() 97 | } 98 | 99 | } 100 | } 101 | ) 102 | .notify(project) 103 | } 104 | 105 | def notifyUnknownError(project: Project, command: String, moduleId: String, scope: DependencyScopeEnum): Unit = { 106 | // add notification 107 | val notification = NotificationGroup 108 | .createNotification( 109 | SbtDependencyAnalyzerBundle.message("analyzer.task.error.title"), 110 | SbtDependencyAnalyzerBundle.message("analyzer.task.error.unknown.text", moduleId, scope.toString, command), 111 | NotificationType.ERROR 112 | ) 113 | .setIcon(SbtDependencyAnalyzerIcons.ICON) 114 | .setImportant(true) 115 | notification.notify(project) 116 | } 117 | 118 | def notifyAndCreateSdapFile(project: Project): Unit = { 119 | // get project/plugins.sbt 120 | // val pluginSbtFileName = "plugins.sbt" 121 | val pluginSbtFileName = "sdap.sbt" 122 | val basePath = VfsUtil.findFile(Path.of(project.getBasePath), true) 123 | implicit val p: Project = project 124 | // 1. get or create sdap.sbt file and add dependency tree statement 125 | inWriteCommandAction { 126 | val sdapText = getSdapText(project) 127 | val projectPath = VfsUtil.createDirectoryIfMissing(basePath, "project") 128 | 129 | var pluginsSbtFile = projectPath.findChild(pluginSbtFileName) 130 | val isSdapAutoGenerate = pluginsSbtFile.isSdapAutoGenerate(sdapText) 131 | if (isSdapAutoGenerate) { 132 | // add to git ignore 133 | val gitExclude = VfsUtil.findRelativeFile(basePath, ".git", "info", "exclude") 134 | val gitExcludeDoc = gitExclude.document() 135 | if (gitExcludeDoc != null) { 136 | val ignoreText = "project" + Constants.SEPARATOR + pluginSbtFileName 137 | if (gitExcludeDoc.getText != null && !gitExcludeDoc.getText.contains(ignoreText)) { 138 | gitExcludeDoc.setReadOnly(false) 139 | gitExcludeDoc.setText( 140 | gitExcludeDoc.getText + Constants.LINE_SEPARATOR + ignoreText + Constants.LINE_SEPARATOR 141 | ) 142 | } 143 | } 144 | pluginsSbtFile = projectPath.findOrCreateChildData(null, pluginSbtFileName) 145 | } 146 | 147 | val doc = pluginsSbtFile.document() 148 | doc.setReadOnly(false) 149 | if (isSdapAutoGenerate) { 150 | doc.setText(sdapText) 151 | } else { 152 | doc.setText(doc.getText + Constants.LINE_SEPARATOR + sdapText) 153 | } 154 | // if intellij not enable auto-reload 155 | // force refresh project 156 | // SbtUtils.refreshProject(project) 157 | // SbtUtils.untilProjectReady(project) 158 | 159 | } 160 | invokeAndWait(SbtUtils.forceRefreshProject(project)) 161 | // 2. add notification 162 | NotificationGroup 163 | .createNotification( 164 | SbtDependencyAnalyzerBundle.message("analyzer.notification.addSdap.title"), 165 | SbtDependencyAnalyzerBundle.message("analyzer.notification.addSdap.text", pluginSbtFileName), 166 | NotificationType.INFORMATION 167 | ) 168 | .setImportant(true) 169 | .setIcon(SbtDependencyAnalyzerIcons.ICON) 170 | .addAction( 171 | new NotificationAction( 172 | SbtDependencyAnalyzerBundle.message("analyzer.notification.gotoSdap", pluginSbtFileName) 173 | ) { 174 | override def actionPerformed(e: AnActionEvent, notification: Notification): Unit = { 175 | inReadAction { 176 | notification.expire() 177 | val recheckFile = VfsUtil.findRelativeFile(basePath, "project", pluginSbtFileName) 178 | if (recheckFile != null) { 179 | FileEditorManager 180 | .getInstance(project) 181 | .openTextEditor(new OpenFileDescriptor(project, recheckFile), true) 182 | } 183 | } 184 | 185 | } 186 | } 187 | ) 188 | .notify(project) 189 | 190 | } 191 | 192 | /** notify information when update plugin 193 | */ 194 | def notifyUpdateActivity(project: Project, version: Version, title: String, content: String): Unit = { 195 | val notification = NotificationGroup 196 | .createNotification(content, NotificationType.INFORMATION) 197 | .setTitle(title) 198 | .setImportant(true) 199 | .setIcon(SbtDependencyAnalyzerIcons.ICON) 200 | .setListenerIfSupport(NotificationListener.URL_OPENING_LISTENER) 201 | if (canBrowseInHTMLEditor) { 202 | notification.whenExpired(() => WhatsNew.browse(version, project)) 203 | } else { 204 | notification.addAction( 205 | new DumbAwareAction( 206 | SbtDependencyAnalyzerBundle.message("analyzer.notification.updated.gotoBrowser"), 207 | null, 208 | AllIcons.General.Web 209 | ) { 210 | override def actionPerformed(e: AnActionEvent): Unit = 211 | notification.expire() 212 | BrowserUtil.browse(WhatsNew.getReleaseNotes(version)) 213 | } 214 | ) 215 | } 216 | notification.notify(project) 217 | if (canBrowseInHTMLEditor && SbtUtils.untilProjectReady(project)) { 218 | waitInterval(10.seconds) 219 | notification.expire() 220 | } 221 | } 222 | 223 | extension (notification: Notification) { 224 | 225 | private def setListenerIfSupport(listener: NotificationListener): Notification = { 226 | try { 227 | org.joor.Reflect.on(notification).call("setListener", listener) 228 | } catch { 229 | case _: Throwable => 230 | // ignore 231 | } 232 | notification 233 | } 234 | } 235 | 236 | extension (file: VirtualFile) { 237 | 238 | private def document(): Document = { 239 | if (file == null) { 240 | return null 241 | } 242 | val doc = FileDocumentManager.getInstance().getDocument(file) 243 | doc 244 | } 245 | 246 | private def isSdapAutoGenerate(sdapText: String): Boolean = { 247 | if (file == null) { 248 | return true 249 | } 250 | val doc = FileDocumentManager.getInstance().getDocument(file) 251 | doc == null || doc.getText == null || doc.getText.trim.isEmpty || doc.getText.trim == sdapText 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/util/SbtDependencyTraverser.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.util; 2 | 3 | import scala.annotation.tailrec 4 | 5 | import org.jetbrains.plugins.scala.extensions.& 6 | import org.jetbrains.plugins.scala.lang.psi.ScalaPsiUtil.inNameContext 7 | import org.jetbrains.plugins.scala.lang.psi.api.{ ScalaElementVisitor, ScalaPsiElement } 8 | import org.jetbrains.plugins.scala.lang.psi.api.base.literals.ScStringLiteral 9 | import org.jetbrains.plugins.scala.lang.psi.api.base.patterns.ScReferencePattern 10 | import org.jetbrains.plugins.scala.lang.psi.api.expr.* 11 | import org.jetbrains.plugins.scala.lang.psi.api.statements.ScPatternDefinition 12 | 13 | import com.intellij.psi.{ PsiElement, PsiFile } 14 | 15 | // copy from https://github.com/JetBrains/intellij-scala/blob/idea242.x/sbt/sbt-impl/src/org/jetbrains/sbt/language/utils/SbtDependencyTraverser.scala 16 | // we have changed some 17 | object SbtDependencyTraverser { 18 | 19 | def traverseStringLiteral(stringLiteral: ScStringLiteral)(callback: PsiElement => Boolean): Unit = 20 | callback(stringLiteral) 21 | 22 | def traverseInfixExpr(infixExpr: ScInfixExpr)(callback: PsiElement => Boolean): Unit = { 23 | if (!callback(infixExpr)) return 24 | 25 | def traverse(expr: ScExpression): Unit = { 26 | expr match { 27 | case subInfix: ScInfixExpr => traverseInfixExpr(subInfix)(callback) 28 | case call: ScMethodCall => traverseMethodCall(call)(callback) 29 | case refExpr: ScReferenceExpression => traverseReferenceExpr(refExpr)(callback) 30 | case stringLiteral: ScStringLiteral => traverseStringLiteral(stringLiteral)(callback) 31 | case blockExpr: ScBlockExpr => traverseBlockExpr(blockExpr)(callback) 32 | case parenthesisedExpr: ScParenthesisedExpr => 33 | // +=("com.chuusai" %%% "shapeless" % shapelessVersion) 34 | traverseParenthesisedExpr(parenthesisedExpr)(callback) 35 | case _ => 36 | } 37 | } 38 | 39 | infixExpr.operation.refName match { 40 | case "++" => 41 | traverse(infixExpr.left) 42 | traverse(infixExpr.right) 43 | case "++=" | ":=" | "+=" => 44 | traverse(infixExpr.right) 45 | case "%" | "%%" => 46 | traverse(infixExpr.left) 47 | traverse(infixExpr.right) 48 | case _ => 49 | traverse(infixExpr.left) 50 | } 51 | } 52 | 53 | def traverseReferenceExpr(refExpr: ScReferenceExpression)(callback: PsiElement => Boolean): Unit = { 54 | if (!callback(refExpr)) return 55 | 56 | refExpr.resolve() match { 57 | case (_: ScReferencePattern) & inNameContext(ScPatternDefinition.expr(expr)) => 58 | expr match { 59 | case infix: ScInfixExpr => 60 | traverseInfixExpr(infix)(callback) 61 | case re: ScReferenceExpression => 62 | traverseReferenceExpr(re)(callback) 63 | case seq: ScMethodCall 64 | if seq.deepestInvokedExpr 65 | .textMatches(SbtDependencyUtils.SEQ) || seq.deepestInvokedExpr.textMatches(SbtDependencyUtils.LIST) => 66 | traverseSeq(seq)(callback) 67 | case stringLiteral: ScStringLiteral => 68 | traverseStringLiteral(stringLiteral)(callback) 69 | 70 | case scParenthesisedExpr: ScParenthesisedExpr => 71 | traverseParenthesisedExpr(scParenthesisedExpr)(callback) 72 | case _ => 73 | } 74 | case _ => 75 | refExpr.acceptChildren( 76 | new ScalaElementVisitor { 77 | override def visitParenthesisedExpr(expr: ScParenthesisedExpr): Unit = 78 | traverseParenthesisedExpr(expr)(callback) 79 | } 80 | ) 81 | } 82 | } 83 | 84 | def traverseMethodCall(call: ScMethodCall)(callback: PsiElement => Boolean): Unit = { 85 | if (!callback(call)) return 86 | 87 | call match { 88 | case seq 89 | if seq.deepestInvokedExpr 90 | .textMatches(SbtDependencyUtils.SEQ) | seq.deepestInvokedExpr.textMatches(SbtDependencyUtils.LIST) => 91 | traverseSeq(seq)(callback) 92 | case settings => 93 | settings.getEffectiveInvokedExpr match { 94 | case expr: ScReferenceExpression if SbtDependencyUtils.isSettings(expr.refName) => 95 | traverseSettings(settings)(callback) 96 | case _ => 97 | } 98 | } 99 | } 100 | 101 | def traversePatternDef(patternDef: ScPatternDefinition)(callback: PsiElement => Boolean): Unit = { 102 | if (!callback(patternDef)) return 103 | 104 | val maybeTypeName = patternDef 105 | .`type`() 106 | .toOption 107 | .map(_.canonicalText) 108 | 109 | if ( 110 | maybeTypeName.contains(SbtDependencyUtils.SBT_PROJECT_TYPE) || maybeTypeName.contains( 111 | SbtDependencyUtils.SBT_CROSS_SETTING_TYPE 112 | ) 113 | ) { 114 | retrieveSettings(patternDef, callback).foreach(traverseMethodCall(_)(callback)) 115 | } else { 116 | patternDef.expr match { 117 | case Some(call: ScMethodCall) => traverseMethodCall(call)(callback) 118 | case Some(infix: ScInfixExpr) => traverseInfixExpr(infix)(callback) 119 | case Some(blockExpr: ScBlockExpr) => traverseBlockExpr(blockExpr)(callback) 120 | case _ => 121 | } 122 | } 123 | } 124 | 125 | /** NOTE: not support `if Seq() + (if x else y)` 126 | */ 127 | def traverseSeq(seq: ScMethodCall)(callback: PsiElement => Boolean): Unit = { 128 | if (!callback(seq)) return 129 | 130 | seq.argumentExpressions.foreach { 131 | case infixExpr: ScInfixExpr => 132 | traverseInfixExpr(infixExpr)(callback) 133 | case refExpr: ScReferenceExpression => 134 | traverseReferenceExpr(refExpr)(callback) 135 | case methodCall: ScMethodCall if methodCall.getEffectiveInvokedExpr.isInstanceOf[ScReferenceExpression] => 136 | val expr = methodCall.getEffectiveInvokedExpr 137 | .asInstanceOf[ScReferenceExpression] 138 | expr 139 | .acceptChildren( // fixed: ("com.chuusai" %%% "shapeless" % shapelessVersion).cross(CrossVersion.for3Use2_13) 140 | new ScalaElementVisitor { 141 | override def visitParenthesisedExpr(expr: ScParenthesisedExpr): Unit = { 142 | traverseParenthesisedExpr(expr)(callback) 143 | } 144 | } 145 | ) 146 | case _ => 147 | } 148 | } 149 | 150 | def traverseParenthesisedExpr(parenthesisedExpr: ScParenthesisedExpr)(callback: PsiElement => Boolean): Unit = { 151 | if (!callback(parenthesisedExpr)) return 152 | 153 | parenthesisedExpr.acceptChildren(new ScalaElementVisitor { 154 | override def visitInfixExpression(infix: ScInfixExpr): Unit = { 155 | traverseInfixExpr(infix)(callback) 156 | } 157 | 158 | override def visitParenthesisedExpr(expr: ScParenthesisedExpr): Unit = 159 | traverseParenthesisedExpr(expr)(callback) 160 | 161 | override def visitMethodCallExpression(call: ScMethodCall): Unit = 162 | call.acceptChildren( 163 | new ScalaElementVisitor { 164 | override def visitReferenceExpression(ref: ScReferenceExpression): Unit = 165 | traverseReferenceExpr(ref)(callback) 166 | } 167 | ) 168 | }) 169 | } 170 | 171 | def traverseBlockExpr(blockExpr: ScBlockExpr)(callback: PsiElement => Boolean): Unit = { 172 | if (!callback(blockExpr)) return 173 | 174 | blockExpr.acceptChildren(new ScalaElementVisitor { 175 | override def visitInfixExpression(infix: ScInfixExpr): Unit = { 176 | traverseInfixExpr(infix)(callback) 177 | } 178 | 179 | override def visitMethodCallExpression(call: ScMethodCall): Unit = { 180 | if ( 181 | call.deepestInvokedExpr.textMatches(SbtDependencyUtils.SEQ) || call.deepestInvokedExpr 182 | .textMatches(SbtDependencyUtils.LIST) 183 | ) 184 | traverseSeq(call)(callback) 185 | } 186 | 187 | override def visitReferenceExpression(ref: ScReferenceExpression): Unit = { 188 | traverseReferenceExpr(ref)(callback) 189 | } 190 | }) 191 | } 192 | 193 | def traverseSettings(settings: ScMethodCall)(callback: PsiElement => Boolean): Unit = { 194 | if (!callback(settings)) return 195 | 196 | settings.args.exprs.foreach { 197 | case infix: ScInfixExpr 198 | if infix.left.textMatches(SbtDependencyUtils.LIBRARY_DEPENDENCIES) && 199 | SbtDependencyUtils.isAddableLibraryDependencies(infix) => 200 | traverseInfixExpr(infix)(callback) 201 | case refExpr: ScReferenceExpression => traverseReferenceExpr(refExpr)(callback) 202 | case _ => 203 | } 204 | } 205 | 206 | @tailrec 207 | def retrievePatternDef(psiElement: PsiElement): ScPatternDefinition = { 208 | psiElement match { 209 | case patternDef: ScPatternDefinition => patternDef 210 | case _: PsiFile => null 211 | case _ => retrievePatternDef(psiElement.getParent) 212 | } 213 | } 214 | 215 | def retrieveSettings(patternDef: ScPatternDefinition, callback: PsiElement => Boolean): Seq[ScMethodCall] = { 216 | var res: Seq[ScMethodCall] = Seq.empty 217 | 218 | def traverse(pd: ScalaPsiElement): Unit = { 219 | pd.acceptChildren(new ScalaElementVisitor { 220 | // NATIVE_SETTINGS,JS_SETTINGS,JVM_SETTINGS,PLATFORM_SETTINGS 221 | override def visitReferenceExpression(ref: ScReferenceExpression): Unit = { 222 | ref.acceptChildren(new ScalaElementVisitor { 223 | override def visitMethodCallExpression(call: ScMethodCall): Unit = { 224 | traverse(call) 225 | super.visitMethodCallExpression(call) 226 | } 227 | }) 228 | super.visitReferenceExpression(ref) 229 | } 230 | 231 | override def visitArgumentExprList(args: ScArgumentExprList): Unit = { 232 | args.acceptChildren( 233 | new ScalaElementVisitor { 234 | override def visitInfixExpression(infix: ScInfixExpr): Unit = { 235 | args.getParent match 236 | case msc: ScMethodCall => 237 | if (msc.`type`().toOption.map(_.canonicalText).contains(SbtDependencyUtils.CROSS_PROJECT)) { 238 | traverseInfixExpr(infix)(callback) 239 | } 240 | super.visitInfixExpression(infix) 241 | } 242 | } 243 | ) 244 | super.visitArgumentExprList(args) 245 | } 246 | 247 | override def visitMethodCallExpression(call: ScMethodCall): Unit = { 248 | call.getEffectiveInvokedExpr match { 249 | case sc: ScMethodCall 250 | if sc.`type`().toOption.map(_.canonicalText).contains(SbtDependencyUtils.CROSS_PROJECT_FUNCTION) => 251 | // platformsSettings(JSPlatform, NativePlatform) \ 252 | // (libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion % Test) 253 | traverse(call) 254 | case expr: ScReferenceExpression if SbtDependencyUtils.isSettings(expr.refName) => 255 | res ++= Seq(call) 256 | case _ => 257 | } 258 | traverse(call.getEffectiveInvokedExpr) 259 | super.visitMethodCallExpression(call) 260 | } 261 | 262 | }) 263 | } 264 | 265 | traverse(patternDef) 266 | 267 | res 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/util/SbtReimportProject.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer.util 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.util.messages.Topic 6 | 7 | object SbtReimportProject { 8 | 9 | val _Topic: Topic[ReimportProjectListener] = 10 | Topic.create("SbtDependencyAnalyzerReimportProject", classOf[ReimportProjectListener]) 11 | 12 | trait ReimportProjectListener: 13 | 14 | def onReimportProject(project: Project): Unit 15 | 16 | end ReimportProjectListener 17 | 18 | val ReimportProjectPublisher: ReimportProjectListener = 19 | ApplicationManager.getApplication.getMessageBus.syncPublisher(_Topic) 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/util/SbtUtils.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package util 5 | 6 | import java.io.* 7 | import java.nio.file.Paths 8 | 9 | import scala.concurrent.duration.* 10 | import scala.jdk.CollectionConverters.* 11 | 12 | import org.jetbrains.sbt.{ SbtUtil as SSbtUtil, SbtVersion } 13 | import org.jetbrains.sbt.project.* 14 | import org.jetbrains.sbt.project.settings.* 15 | import org.jetbrains.sbt.settings.SbtSettings 16 | 17 | import com.intellij.openapi.application.ApplicationManager 18 | import com.intellij.openapi.diagnostic.Logger 19 | import com.intellij.openapi.externalSystem.autoimport.{ 20 | ExternalSystemProjectNotificationAware, 21 | ExternalSystemProjectTracker 22 | } 23 | import com.intellij.openapi.externalSystem.importing.ImportSpecBuilder 24 | import com.intellij.openapi.externalSystem.model.project.dependencies.DependencyNode 25 | import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType 26 | import com.intellij.openapi.externalSystem.service.internal.ExternalSystemProcessingManager 27 | import com.intellij.openapi.externalSystem.service.project.trusted.ExternalSystemTrustedProjectDialog 28 | import com.intellij.openapi.externalSystem.util.{ ExternalSystemApiUtil, ExternalSystemUtil } 29 | import com.intellij.openapi.module.ModuleManager 30 | import com.intellij.openapi.project.* 31 | import com.intellij.openapi.roots.OrderRootType 32 | import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar 33 | 34 | object SbtUtils { 35 | 36 | private val LOG = Logger.getInstance(getClass) 37 | 38 | /** sbt: com.softwaremill.sttp.shared:zio_3:1.3.7:jar 39 | */ 40 | def getLibrarySize(project: Project, artifact: String): Long = { 41 | val libraryTable = LibraryTablesRegistrar.getInstance.getLibraryTable(project) 42 | val library = libraryTable.getLibraryByName(s"sbt: $artifact:jar") 43 | if (library == null) return 0 44 | val vf = library.getFiles(OrderRootType.CLASSES) 45 | if (vf != null) { 46 | vf.headOption.map(_.getLength).getOrElse(0) 47 | } else 0 48 | } 49 | 50 | def getLibraryTotalSize(project: Project, ds: List[DependencyNode]): Long = { 51 | if (ds.isEmpty) return 0L 52 | ds.map(d => 53 | getLibrarySize(project, d.getDisplayName) + getLibraryTotalSize(project, d.getDependencies.asScala.toList) 54 | ).sum 55 | } 56 | 57 | def getSbtProject(project: Project): SbtSettings = SSbtUtil.sbtSettings(project) 58 | 59 | def forceRefreshProject(project: Project): Unit = { 60 | ExternalSystemUtil.refreshProjects( 61 | new ImportSpecBuilder(project, SbtProjectSystem.Id) 62 | .dontNavigateToError() 63 | .dontReportRefreshErrors() 64 | .build() 65 | ) 66 | } 67 | 68 | def untilProjectReady(project: Project): Boolean = { 69 | val timeout = 10.minutes 70 | val interval = 100.millis 71 | val startTime = System.currentTimeMillis() 72 | val endTime = startTime + timeout.toMillis 73 | while (System.currentTimeMillis() < endTime && !SbtUtils.isProjectReady(project)) { 74 | waitInterval(interval) 75 | } 76 | true 77 | } 78 | 79 | // TODO 80 | private def isProjectReady(project: Project): Boolean = { 81 | SbtUtils 82 | .getExternalProjectPath(project) 83 | .map { externalProjectPath => 84 | // index is ready? 85 | val processingManager = ApplicationManager.getApplication.getService(classOf[ExternalSystemProcessingManager]) 86 | if ( 87 | processingManager 88 | .findTask(ExternalSystemTaskType.RESOLVE_PROJECT, SbtProjectSystem.Id, externalProjectPath) != null 89 | || processingManager 90 | .findTask(ExternalSystemTaskType.REFRESH_TASKS_LIST, SbtProjectSystem.Id, externalProjectPath) != null 91 | ) { 92 | false 93 | } else true 94 | } 95 | .forall(identity) 96 | } 97 | 98 | def getExternalProjectPath(project: Project): List[String] = 99 | getSbtProject(project).getLinkedProjectsSettings.asScala.map(_.getExternalProjectPath()).toList 100 | 101 | def getSbtExecutionSettings(dir: String, project: Project): SbtExecutionSettings = 102 | SbtExternalSystemManager.executionSettingsFor(project, dir) 103 | 104 | def launcherJar(sbtSettings: SbtExecutionSettings): File = 105 | sbtSettings.customLauncher.getOrElse(SSbtUtil.getDefaultLauncher) 106 | 107 | def getSbtVersion(project: Project): SbtVersion = { 108 | val workingDirPath = getWorkingDirPath(project) 109 | val sbtSettings = getSbtExecutionSettings(workingDirPath, project) 110 | lazy val launcher = launcherJar(sbtSettings) 111 | SSbtUtil.detectSbtVersion(Paths.get(workingDirPath), launcher.toPath) 112 | } 113 | 114 | // see https://github.com/JetBrains/intellij-scala/blob/idea232.x/sbt/sbt-impl/src/org/jetbrains/sbt/shell/SbtProcessManager.scala 115 | def getWorkingDirPath(project: Project): String = { 116 | // Fist try to calculate root path based on `getExternalRootProjectPath` 117 | // When sbt project reference another sbt project via `RootProject` this will correctly find the root project path (see SCL-21143) 118 | // However, if user manually linked multiple SBT projects via external system tool window (sbt tool window) 119 | // using "Link sbt Project" button (the one with "plus" icon), it will randomly choose one of the projects 120 | val externalRootProjectPath: Option[String] = { 121 | val modules = ModuleManager.getInstance(project).getModules.toSeq 122 | modules.iterator.map(ExternalSystemApiUtil.getExternalRootProjectPath).find(_ != null) 123 | } 124 | externalRootProjectPath.orElse { 125 | // Not sure when externalRootProjectPath can be empty in SBT projects 126 | // But just in case fallback to ProjectUtil.guessProjectDir, but notice that it's not reliable in some cases (see SCL-21143) 127 | val message = 128 | s"Can't calculate external root project path for project `${project.getName}`, fallback to `ProjectUtil.guessProjectDir`" 129 | if (ApplicationManager.getApplication.isInternal) 130 | LOG.error(message) 131 | else 132 | LOG.warn(message) 133 | Option(ProjectUtil.guessProjectDir(project)).map(_.getCanonicalPath) 134 | } 135 | .getOrElse(throw new IllegalStateException(s"no project directory found for project ${project.getName}")) 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/util/packagesearch/AddDependencyPreviewWizard.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package util 5 | package packagesearch 6 | 7 | import org.jetbrains.sbt.language.utils.{ DependencyOrRepositoryPlaceInfo, SbtArtifactInfo } 8 | 9 | import com.intellij.ide.wizard.{ AbstractWizard, Step } 10 | import com.intellij.openapi.project.Project 11 | 12 | // copy from https://github.com/JetBrains/intellij-scala/tree/idea242.x/scala/integration/packagesearch/src/org/jetbrains/plugins/scala/packagesearch/ui 13 | class AddDependencyPreviewWizard( 14 | project: Project, 15 | elem: SbtArtifactInfo, 16 | fileLines: Seq[DependencyOrRepositoryPlaceInfo] 17 | ) extends AbstractWizard[Step]( 18 | SbtDependencyAnalyzerBundle.message( 19 | "analyzer.packagesearch.dependency.sbt.possible.places.to.add.new.dependency" 20 | ), 21 | project 22 | ) { 23 | 24 | private val sbtPossiblePlacesStep = new SbtPossiblePlacesStep(this, project, fileLines) 25 | 26 | val elementToAdd: Any = elem 27 | var resultFileLine: Option[DependencyOrRepositoryPlaceInfo] = None 28 | 29 | override def getHelpID: String = null 30 | 31 | def search(): Option[DependencyOrRepositoryPlaceInfo] = { 32 | if (!showAndGet()) { 33 | return None 34 | } 35 | resultFileLine 36 | } 37 | 38 | addStep(sbtPossiblePlacesStep) 39 | init() 40 | 41 | override def dispose(): Unit = { 42 | sbtPossiblePlacesStep.panel.releaseEditor() 43 | super.dispose() 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/util/packagesearch/SbtDependencyModifier.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package util 5 | package packagesearch 6 | 7 | import java.util 8 | import java.util.Collections.emptyList 9 | 10 | import scala.jdk.CollectionConverters.* 11 | 12 | import bitlap.sbt.analyzer.model.AnalyzerCommandNotFoundException 13 | import bitlap.sbt.analyzer.util.SbtDependencyUtils.* 14 | import bitlap.sbt.analyzer.util.SbtDependencyUtils.GetMode.* 15 | 16 | import org.jetbrains.plugins.scala.extensions.* 17 | import org.jetbrains.plugins.scala.lang.psi.api.ScalaFile 18 | import org.jetbrains.plugins.scala.lang.psi.api.base.literals.ScStringLiteral 19 | import org.jetbrains.plugins.scala.lang.psi.api.expr.* 20 | import org.jetbrains.plugins.scala.lang.psi.impl.ScalaCode.* 21 | import org.jetbrains.plugins.scala.lang.psi.impl.ScalaPsiElementFactory 22 | import org.jetbrains.plugins.scala.project.{ ProjectContext, ProjectPsiFileExt, ScalaFeatures } 23 | import org.jetbrains.sbt.SbtUtil 24 | import org.jetbrains.sbt.language.utils.{ DependencyOrRepositoryPlaceInfo, SbtArtifactInfo, SbtDependencyCommon } 25 | import org.jetbrains.sbt.language.utils.SbtDependencyCommon.defaultLibScope 26 | import org.jetbrains.sbt.resolvers.{ SbtMavenResolver, SbtResolverUtils } 27 | 28 | import com.intellij.buildsystem.model.DeclaredDependency 29 | import com.intellij.buildsystem.model.unified.{ UnifiedCoordinates, UnifiedDependency, UnifiedDependencyRepository } 30 | import com.intellij.externalSystem.ExternalDependencyModificator 31 | import com.intellij.openapi.application.ApplicationManager 32 | import com.intellij.openapi.diagnostic.{ ControlFlowException, Logger } 33 | import com.intellij.openapi.module as OpenapiModule 34 | import com.intellij.openapi.project.Project 35 | import com.intellij.psi.PsiManager 36 | 37 | // copy from https://github.com/JetBrains/intellij-scala/blob/idea242.x/scala/integration/packagesearch/src/org/jetbrains/plugins/scala/packagesearch/SbtDependencyModifier.scala 38 | object SbtDependencyModifier extends ExternalDependencyModificator { 39 | 40 | private val logger = Logger.getInstance(this.getClass) 41 | 42 | override def supports(module: OpenapiModule.Module): Boolean = SbtUtil.isSbtModule(module) 43 | 44 | override def addDependency(module: OpenapiModule.Module, newDependency: UnifiedDependency): Unit = { 45 | implicit val project: Project = module.getProject 46 | val sbtFileOpt = SbtDependencyUtils.getSbtFileOpt(module) 47 | if (sbtFileOpt == null) return 48 | val dependencyPlaces = inReadAction(for { 49 | sbtFile <- sbtFileOpt 50 | psiSbtFile = PsiManager.getInstance(project).findFile(sbtFile).asInstanceOf[ScalaFile] 51 | sbtFileModule = psiSbtFile.module.orNull 52 | topLevelPlace = 53 | if ( 54 | sbtFileModule != null && (sbtFileModule == module || sbtFileModule.getName == s"""${module.getName}-build""") 55 | ) 56 | Seq(SbtDependencyUtils.getTopLevelPlaceToAdd(psiSbtFile)) 57 | else Seq.empty 58 | 59 | depPlaces = (SbtDependencyUtils 60 | .getLibraryDependenciesOrPlaces(sbtFileOpt, project, module, GetPlace) 61 | .map(psiAndString => SbtDependencyUtils.toDependencyPlaceInfo(psiAndString._1, Seq())) 62 | ++ topLevelPlace).map { 63 | case Some(inside: DependencyOrRepositoryPlaceInfo) => inside 64 | case _ => null 65 | }.filter(_ != null).sortWith(_.toString < _.toString) 66 | } yield depPlaces).getOrElse(Seq.empty) 67 | val newDependencyCoordinates = newDependency.getCoordinates 68 | val newArtifactInfo = SbtArtifactInfo( 69 | newDependencyCoordinates.getGroupId, 70 | newDependencyCoordinates.getArtifactId, 71 | newDependencyCoordinates.getVersion, 72 | newDependency.getScope 73 | ) 74 | 75 | ApplicationManager.getApplication.invokeLater { () => 76 | val wizard = new AddDependencyPreviewWizard(project, newArtifactInfo, dependencyPlaces) 77 | wizard.search() match { 78 | case Some(fileLine) => 79 | SbtDependencyUtils.addDependency(fileLine.element, newArtifactInfo)(project) 80 | case _ => 81 | } 82 | } 83 | } 84 | 85 | override def updateDependency( 86 | module: OpenapiModule.Module, 87 | currentDependency: UnifiedDependency, 88 | newDependency: UnifiedDependency 89 | ): Unit = { 90 | implicit val project: Project = module.getProject 91 | val targetedLibDepTuple = 92 | SbtDependencyUtils.findLibraryDependency(project, module, currentDependency, configurationRequired = false) 93 | if (targetedLibDepTuple == null) return 94 | val oldLibDep = SbtDependencyUtils.processLibraryDependencyFromExprAndString(targetedLibDepTuple, preserve = true) 95 | val newCoordinates = newDependency.getCoordinates 96 | 97 | if ( 98 | SbtDependencyUtils.cleanUpDependencyPart( 99 | oldLibDep(2).asInstanceOf[ScStringLiteral].getText 100 | ) != newCoordinates.getVersion 101 | ) { 102 | inWriteCommandAction { 103 | val literal = oldLibDep(2).asInstanceOf[ScStringLiteral] 104 | literal 105 | .replace( 106 | ScalaPsiElementFactory.createElementFromText(s""""${newCoordinates.getVersion}"""", literal) 107 | ) 108 | } 109 | return 110 | } 111 | var oldConfiguration = "" 112 | if (targetedLibDepTuple._2 != "") 113 | oldConfiguration = SbtDependencyUtils.cleanUpDependencyPart(targetedLibDepTuple._2) 114 | 115 | if (oldLibDep.length > 3) 116 | oldConfiguration = SbtDependencyUtils.cleanUpDependencyPart(oldLibDep(3).asInstanceOf[String]) 117 | val newConfiguration = if (newDependency.getScope != defaultLibScope) newDependency.getScope else "" 118 | if (oldConfiguration.toLowerCase != newConfiguration.toLowerCase) { 119 | if (targetedLibDepTuple._2 != "") { 120 | if (newConfiguration == "") { 121 | inWriteCommandAction(targetedLibDepTuple._3.replace(code"${targetedLibDepTuple._3.left.getText}")) 122 | } else { 123 | inWriteCommandAction(targetedLibDepTuple._3.right.replace(code"${newConfiguration}")) 124 | } 125 | 126 | } else { 127 | if (oldLibDep.length > 3) { 128 | if (newConfiguration == "") { 129 | inWriteCommandAction(targetedLibDepTuple._1.replace(code"${targetedLibDepTuple._1.left}")) 130 | } else { 131 | inWriteCommandAction(targetedLibDepTuple._1.right.replace(code"""${newConfiguration}""")) 132 | } 133 | } else { 134 | if (newConfiguration != "") { 135 | inWriteCommandAction( 136 | targetedLibDepTuple._1.replace(code"""${targetedLibDepTuple._1.getText} % $newConfiguration""") 137 | ) 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | override def removeDependency(module: OpenapiModule.Module, toRemoveDependency: UnifiedDependency): Unit = { 145 | implicit val project: Project = module.getProject 146 | val targetedLibDepTuple = 147 | SbtDependencyUtils.findLibraryDependency(project, module, toRemoveDependency, configurationRequired = false) 148 | if (targetedLibDepTuple == null) { 149 | throw AnalyzerCommandNotFoundException("Target dependency not found") 150 | } 151 | targetedLibDepTuple._3.getParent match { 152 | case _: ScArgumentExprList => 153 | inWriteCommandAction { 154 | targetedLibDepTuple._3.delete() 155 | } 156 | case infix: ScInfixExpr if infix.left.textMatches(SbtDependencyUtils.LIBRARY_DEPENDENCIES) => 157 | inWriteCommandAction { 158 | infix.delete() 159 | } 160 | case infix: ScParenthesisedExpr if infix.parents.toList.exists(_.isInstanceOf[ScReferenceExpression]) => 161 | val lastRef = infix.parents.toList.filter(_.isInstanceOf[ScReferenceExpression]).lastOption 162 | inWriteCommandAction { 163 | lastRef.foreach(_.parent.foreach(_.delete())) 164 | } 165 | case _ => 166 | throw AnalyzerCommandNotFoundException("Target parent not found") 167 | } 168 | } 169 | 170 | override def addRepository( 171 | module: OpenapiModule.Module, 172 | unifiedDependencyRepository: UnifiedDependencyRepository 173 | ): Unit = { 174 | implicit val project: Project = module.getProject 175 | val sbtFileOpt = SbtDependencyUtils.getSbtFileOpt(module) 176 | if (sbtFileOpt == null) return 177 | val sbtFile = sbtFileOpt.orNull 178 | if (sbtFile == null) return 179 | val psiSbtFile = PsiManager.getInstance(project).findFile(sbtFile).asInstanceOf[ScalaFile] 180 | 181 | SbtDependencyUtils.addRepository(psiSbtFile, unifiedDependencyRepository) 182 | } 183 | 184 | override def deleteRepository( 185 | module: OpenapiModule.Module, 186 | unifiedDependencyRepository: UnifiedDependencyRepository 187 | ): Unit = {} 188 | 189 | override def declaredDependencies(module: OpenapiModule.Module): util.List[DeclaredDependency] = 190 | SbtDependencyUtils.declaredDependencies(module) 191 | 192 | override def declaredRepositories(module: OpenapiModule.Module): util.List[UnifiedDependencyRepository] = try { 193 | SbtResolverUtils 194 | .projectResolvers(module.getProject) 195 | .collect { case r: SbtMavenResolver => 196 | new UnifiedDependencyRepository(r.name, r.presentableName, r.normalizedRoot) 197 | } 198 | .toList 199 | .asJava 200 | } catch { 201 | case c: ControlFlowException => throw c 202 | case e: Exception => 203 | logger.error( 204 | s"Error occurs when obtaining the list of supported repositories/resolvers for module ${module.getName} using package search plugin", 205 | e 206 | ) 207 | emptyList() 208 | } 209 | 210 | final def addExcludeToDependency( 211 | module: OpenapiModule.Module, 212 | currentDependency: UnifiedDependency, 213 | coordinates: UnifiedCoordinates 214 | ): Boolean = { 215 | implicit val project: Project = module.getProject 216 | val targetedLibDepTuple = 217 | SbtDependencyUtils.findLibraryDependency(project, module, currentDependency, configurationRequired = false) 218 | if (targetedLibDepTuple == null) return false 219 | // add `(expr).exclude('group', 'artifact')` 220 | inWriteCommandAction { 221 | val newExpr = wrapInParentheses(targetedLibDepTuple._3) 222 | val newCode = s"""${newExpr.getText}.${ScalaPsiElementFactory 223 | .createNewLine() 224 | .getText}exclude("${coordinates.getGroupId}", "${coordinates.getArtifactId}")""" 225 | targetedLibDepTuple._3.replace(code"""$newCode""") 226 | } 227 | true 228 | } 229 | 230 | private def wrapInParentheses(expression: ScExpression)(implicit ctx: ProjectContext): ScParenthesisedExpr = { 231 | val parenthesised = ScalaPsiElementFactory 232 | .createElementFromText[ScParenthesisedExpr](expression.getText.parenthesize(true), expression) 233 | parenthesised.innerElement.foreach(_.replace(expression.copy())) 234 | parenthesised 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/util/packagesearch/SbtPossiblePlacesPanel.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package util 5 | package packagesearch 6 | 7 | import java.awt.BorderLayout 8 | import javax.swing.{ JList, JPanel, JSplitPane, ListSelectionModel, ScrollPaneConstants } 9 | import javax.swing.event.ListSelectionEvent 10 | 11 | import scala.jdk.CollectionConverters.IterableHasAsJava 12 | 13 | import org.jetbrains.plugins.scala.ScalaFileType 14 | import org.jetbrains.plugins.scala.extensions.inWriteAction 15 | import org.jetbrains.plugins.scala.lang.psi.impl.ScalaPsiElementFactory 16 | import org.jetbrains.plugins.scala.project.ProjectExt 17 | import org.jetbrains.sbt.language 18 | import org.jetbrains.sbt.language.utils.{ DependencyOrRepositoryPlaceInfo, SbtArtifactInfo } 19 | 20 | import com.intellij.buildsystem.model.unified.UnifiedDependencyRepository 21 | import com.intellij.openapi.editor.{ Editor, EditorFactory, LogicalPosition, ScrollType } 22 | import com.intellij.openapi.editor.colors.CodeInsightColors 23 | import com.intellij.openapi.editor.ex.EditorEx 24 | import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory 25 | import com.intellij.openapi.editor.markup.{ HighlighterLayer, HighlighterTargetArea } 26 | import com.intellij.openapi.fileEditor.FileDocumentManager 27 | import com.intellij.openapi.project.Project 28 | import com.intellij.psi.PsiElement 29 | import com.intellij.ui.{ 30 | CollectionListModel, 31 | ColoredListCellRenderer, 32 | GuiUtils, 33 | ScrollPaneFactory, 34 | SimpleTextAttributes 35 | } 36 | import com.intellij.ui.components.JBList 37 | 38 | // copy from https://github.com/JetBrains/intellij-scala/tree/idea242.x/scala/integration/packagesearch/src/org/jetbrains/plugins/scala/packagesearch/ui 39 | private class SbtPossiblePlacesPanel( 40 | project: Project, 41 | wizard: AddDependencyPreviewWizard, 42 | fileLines: Seq[DependencyOrRepositoryPlaceInfo] 43 | ) extends JPanel { 44 | val myResultList: JBList[DependencyOrRepositoryPlaceInfo] = new JBList[DependencyOrRepositoryPlaceInfo]() 45 | var myCurEditor: Editor = createEditor() 46 | 47 | private val EDITOR_TOP_MARGIN = 7 48 | 49 | init() 50 | 51 | def init(): Unit = { 52 | setLayout(new BorderLayout()) 53 | 54 | val splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT) 55 | 56 | myResultList.setModel(new CollectionListModel[DependencyOrRepositoryPlaceInfo](fileLines.asJavaCollection)) 57 | myResultList.getSelectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION) 58 | 59 | val scrollPane = ScrollPaneFactory.createScrollPane(myResultList) 60 | scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS) // Don't remove this line. 61 | scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS) 62 | splitPane.setContinuousLayout(true) 63 | splitPane.add(scrollPane) 64 | splitPane.add(myCurEditor.getComponent) 65 | 66 | add(splitPane, BorderLayout.CENTER) 67 | 68 | GuiUtils.replaceJSplitPaneWithIDEASplitter(splitPane) 69 | myResultList.setCellRenderer(new PlacesCellRenderer) 70 | 71 | myResultList.addListSelectionListener { (_: ListSelectionEvent) => 72 | val place = myResultList.getSelectedValue 73 | if (place != null) { 74 | if (myCurEditor == null) 75 | myCurEditor = createEditor() 76 | updateEditor(place) 77 | } 78 | wizard.updateButtons(true, place != null, false) 79 | } 80 | } 81 | 82 | private def updateEditor(myCurFileLine: DependencyOrRepositoryPlaceInfo): Unit = { 83 | val document = 84 | FileDocumentManager.getInstance.getDocument(project.baseDir.findFileByRelativePath(myCurFileLine.path)) 85 | val tmpFile = ScalaPsiElementFactory.createScalaFileFromText(document.getText, myCurFileLine.element)(project) 86 | var tmpElement = tmpFile.findElementAt(myCurFileLine.element.getTextOffset) 87 | while (tmpElement.getTextRange != myCurFileLine.element.getTextRange) { 88 | tmpElement = tmpElement.getParent 89 | } 90 | 91 | var dep: Option[PsiElement] = null 92 | wizard.elementToAdd match { 93 | case info: SbtArtifactInfo => 94 | dep = SbtDependencyUtils.addDependency(tmpElement, info)(project) 95 | case repo: UnifiedDependencyRepository => 96 | dep = SbtDependencyUtils.addRepository(tmpElement, repo)(project) 97 | } 98 | 99 | inWriteAction { 100 | myCurEditor.getDocument.setText { 101 | if (dep.isDefined) tmpFile.getText 102 | else 103 | SbtDependencyAnalyzerBundle.message( 104 | "analyzer.packagesearch.dependency.sbt.could.not.generate.expression.string.to.add" 105 | ) 106 | } 107 | } 108 | 109 | myCurEditor.getCaretModel.moveToOffset(myCurFileLine.offset) 110 | val scrollingModel = myCurEditor.getScrollingModel 111 | val oldPos = myCurEditor.offsetToLogicalPosition(myCurFileLine.offset) 112 | scrollingModel.scrollTo( 113 | new LogicalPosition(math.max(1, oldPos.line - EDITOR_TOP_MARGIN), oldPos.column), 114 | ScrollType.CENTER 115 | ) 116 | val attributes = myCurEditor.getColorsScheme.getAttributes(CodeInsightColors.MATCHED_BRACE_ATTRIBUTES) 117 | 118 | val (startOffset, endOffset) = dep match { 119 | case Some(elem) => 120 | (elem.getTextRange.getStartOffset, elem.getTextRange.getEndOffset) 121 | case None => (0, 0) 122 | } 123 | // Reset all highlighters (if exist) 124 | myCurEditor.getMarkupModel.removeAllHighlighters() 125 | myCurEditor.getMarkupModel.addRangeHighlighter( 126 | startOffset, 127 | endOffset, 128 | HighlighterLayer.SELECTION, 129 | attributes, 130 | HighlighterTargetArea.EXACT_RANGE 131 | ) 132 | } 133 | 134 | def releaseEditor(): Unit = { 135 | if (myCurEditor != null && !myCurEditor.isDisposed) { 136 | EditorFactory.getInstance.releaseEditor(myCurEditor) 137 | myCurEditor = null 138 | } 139 | } 140 | 141 | private def createEditor(): Editor = { 142 | val viewer = EditorFactory.getInstance.createViewer(EditorFactory.getInstance().createDocument("")) 143 | val editorHighlighter = 144 | EditorHighlighterFactory.getInstance.createEditorHighlighter(project, ScalaFileType.INSTANCE) 145 | viewer.asInstanceOf[EditorEx].setHighlighter(editorHighlighter) 146 | viewer 147 | } 148 | 149 | private class PlacesCellRenderer extends ColoredListCellRenderer[DependencyOrRepositoryPlaceInfo] { 150 | 151 | // noinspection ReferencePassedToNls,ScalaExtractStringToBundle 152 | override def customizeCellRenderer( 153 | list: JList[? <: DependencyOrRepositoryPlaceInfo], 154 | info: DependencyOrRepositoryPlaceInfo, 155 | index: Int, 156 | selected: Boolean, 157 | hasFocus: Boolean 158 | ): Unit = { 159 | setIcon(language.SbtFileType.getIcon) 160 | append(info.path + ":") 161 | append(info.line.toString, SimpleTextAttributes.GRAY_ATTRIBUTES) 162 | if (info.affectedProjects.nonEmpty) 163 | append(" (" + info.affectedProjects.mkString(", ") + ")") 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/scala/bitlap/sbt/analyzer/util/packagesearch/SbtPossiblePlacesStep.scala: -------------------------------------------------------------------------------- 1 | package bitlap 2 | package sbt 3 | package analyzer 4 | package util 5 | package packagesearch 6 | 7 | import javax.swing.{ Icon, JComponent } 8 | 9 | import org.jetbrains.plugins.scala.extensions 10 | import org.jetbrains.sbt.language.utils.DependencyOrRepositoryPlaceInfo 11 | 12 | import com.intellij.ide.wizard.Step 13 | import com.intellij.openapi.project.Project 14 | import com.intellij.ui.scale.JBUIScale 15 | 16 | // copy from https://github.com/JetBrains/intellij-scala/tree/idea242.x/scala/integration/packagesearch/src/org/jetbrains/plugins/scala/packagesearch/ui 17 | private class SbtPossiblePlacesStep( 18 | wizard: AddDependencyPreviewWizard, 19 | project: Project, 20 | fileLines: Seq[DependencyOrRepositoryPlaceInfo] 21 | ) extends Step { 22 | 23 | val panel = new SbtPossiblePlacesPanel(project, wizard, fileLines) 24 | 25 | override def _init(): Unit = { 26 | wizard.setSize(JBUIScale.scale(800), JBUIScale.scale(750)) 27 | panel.myResultList.clearSelection() 28 | extensions.inWriteAction { 29 | panel.myCurEditor.getDocument.setText( 30 | SbtDependencyAnalyzerBundle.message( 31 | "analyzer.packagesearch.dependency.sbt.select.a.place.from.the.list.above.to.enable.this.preview" 32 | ) 33 | ) 34 | } 35 | panel.updateUI() 36 | } 37 | 38 | override def getComponent: JComponent = panel 39 | 40 | override def _commit(finishChosen: Boolean): Unit = { 41 | if (finishChosen) { 42 | wizard.resultFileLine = Option(panel.myResultList.getSelectedValue) 43 | } 44 | } 45 | 46 | override def getIcon: Icon = null 47 | 48 | override def getPreferredFocusedComponent: JComponent = panel 49 | } 50 | -------------------------------------------------------------------------------- /src/test/scala/bitlap/sbt/analyzer/AnalyzedDotFileParserSpec.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer 2 | 3 | import bitlap.sbt.analyzer.model.ModuleContext 4 | import bitlap.sbt.analyzer.parser.{ AnalyzedFileType, AnalyzedParserFactory } 5 | 6 | import org.scalatest.flatspec.AnyFlatSpec 7 | 8 | import com.intellij.openapi.externalSystem.model.project.dependencies.* 9 | 10 | class AnalyzedDotFileParserSpec extends AnyFlatSpec { 11 | 12 | "parse dot file" should "convert to object successfully " in { 13 | val start = System.currentTimeMillis() 14 | 15 | val root = new DependencyScopeNode( 16 | 0, 17 | "compile", 18 | "compile", 19 | "compile" 20 | ) 21 | root.setResolutionState(ResolutionState.RESOLVED) 22 | 23 | val ctx = 24 | ModuleContext( 25 | getClass.getClassLoader.getResource("test.dot").getFile, 26 | "star-authority-protocol", 27 | DependencyScopeEnum.Compile, 28 | "fc.xuanwu.star", 29 | ideaModuleNamePaths = Map.empty, 30 | ideaModuleIdSbtModuleNames = Map.empty, 31 | isTest = true 32 | ) 33 | 34 | val relations = AnalyzedParserFactory 35 | .getInstance(AnalyzedFileType.Dot) 36 | .buildDependencyTree(ctx, root) 37 | 38 | println(s"analyze dot cost:${System.currentTimeMillis() - start}ms") 39 | 40 | assert(relations.getDependencies.size() > 0) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/scala/bitlap/sbt/analyzer/DotUtilSpec.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer 2 | 3 | import java.util 4 | import java.util.concurrent.atomic.AtomicInteger 5 | 6 | import scala.jdk.CollectionConverters.* 7 | 8 | import bitlap.sbt.analyzer.model.* 9 | import bitlap.sbt.analyzer.parser.DotUtil 10 | import bitlap.sbt.analyzer.util.DependencyUtils 11 | 12 | import org.scalatest.flatspec.AnyFlatSpec 13 | 14 | import guru.nidi.graphviz.model.* 15 | 16 | class DotUtilSpec extends AnyFlatSpec { 17 | 18 | "parse file as MutableNode" should "ok" in { 19 | val start = System.currentTimeMillis() 20 | val file = getClass.getClassLoader.getResource("test.dot").getFile 21 | val ctx = 22 | ModuleContext( 23 | file, 24 | "star-authority-protocol", 25 | DependencyScopeEnum.Compile, 26 | "fc.xuanwu.star", 27 | ideaModuleNamePaths = Map.empty, 28 | isScalaJs = false, 29 | isScalaNative = false, 30 | ideaModuleIdSbtModuleNames = Map.empty, 31 | isTest = true 32 | ) 33 | 34 | val mutableGraph: MutableGraph = DotUtil.parseAsGraph(ctx) 35 | val graphNodes: util.Collection[MutableNode] = mutableGraph.nodes() 36 | val links: util.Collection[Link] = mutableGraph.edges() 37 | 38 | val nodes = graphNodes.asScala.map { graphNode => 39 | graphNode.name().value() -> DependencyUtils.getArtifactInfoFromDisplayName(graphNode.name().value()) 40 | }.collect { case (name, Some(value)) => 41 | name -> value 42 | }.toMap 43 | val idMapping: Map[String, Int] = nodes.map(kv => kv._2.toString -> kv._2.id) 44 | 45 | val edges = links.asScala.map { l => 46 | val label = l.get("label").asInstanceOf[String] 47 | Relation( 48 | idMapping.getOrElse(l.from().name().value(), 0), 49 | idMapping.getOrElse(l.to().name().value(), 0), 50 | label 51 | ) 52 | } 53 | 54 | println(s"parse dot cost:${System.currentTimeMillis() - start}ms") 55 | assert(nodes.size == 69) 56 | assert(edges.size == 146) 57 | 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/scala/bitlap/sbt/analyzer/SbtShellTaskRegex.scala: -------------------------------------------------------------------------------- 1 | package bitlap.sbt.analyzer 2 | 3 | import bitlap.sbt.analyzer.task.SbtShellOutputAnalysisTask 4 | 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | 7 | class SbtShellTaskRegex extends AnyFlatSpec { 8 | 9 | "regex match" should "ok" in { 10 | "[info] \torg.bitlap" match 11 | case SbtShellOutputAnalysisTask.SHELL_OUTPUT_RESULT_REGEX(_, _, org) => 12 | assert(org.trim == "org.bitlap") 13 | case _ => assert(false) 14 | 15 | "[info] rolls-csv / moduleName" match 16 | case SbtShellOutputAnalysisTask.MODULE_NAME_INPUT_REGEX(_, _, moduleName, _, _) => 17 | assert(moduleName.trim == "rolls-csv") 18 | case _ => assert(false) 19 | 20 | "[info] moduleName" match 21 | case SbtShellOutputAnalysisTask.ROOT_MODULE_NAME_INPUT_REGEX(_, _) => 22 | assert(true) 23 | case _ => assert(false) 24 | } 25 | 26 | } 27 | --------------------------------------------------------------------------------