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 |
--------------------------------------------------------------------------------