diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 8e24c2ee..fce20363 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -321,6 +321,9 @@
+
+
@@ -333,8 +336,6 @@
-
-
diff --git a/src/main/resources/icons/sortById.svg b/src/main/resources/icons/sortById.svg
deleted file mode 100644
index b6eddab2..00000000
--- a/src/main/resources/icons/sortById.svg
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
diff --git a/src/main/resources/icons/sortById_dark.svg b/src/main/resources/icons/sortById_dark.svg
deleted file mode 100644
index 8c3af103..00000000
--- a/src/main/resources/icons/sortById_dark.svg
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
diff --git a/src/main/scala/zio/intellij/gutter/ForkedCodeLineMarkerProvider.scala b/src/main/scala/zio/intellij/gutter/ForkedCodeLineMarkerProvider.scala
index d6c2dfb5..cead7726 100644
--- a/src/main/scala/zio/intellij/gutter/ForkedCodeLineMarkerProvider.scala
+++ b/src/main/scala/zio/intellij/gutter/ForkedCodeLineMarkerProvider.scala
@@ -5,6 +5,7 @@ import com.intellij.openapi.editor.markup.GutterIconRenderer.Alignment
import com.intellij.psi.PsiElement
import org.jetbrains.plugins.scala.lang.lexer.ScalaTokenTypes
import org.jetbrains.plugins.scala.lang.psi.api.expr.ScReferenceExpression
+import zio.intellij.icons.FiberIcon
import zio.intellij.inspections._
import zio.intellij.inspections.zioMethods._
@@ -41,7 +42,7 @@ object ForkedCodeLineMarkerProvider {
new LineMarkerInfo(
element,
element.getTextRange,
- fiberIcon,
+ FiberIcon,
(_: PsiElement) => s"Effect is explicitly forked via '${element.getText}'",
null, // the handler executed when the gutter icon is clicked
Alignment.LEFT,
diff --git a/src/main/scala/zio/intellij/gutter/ZLayerLineMarkerProvider.scala b/src/main/scala/zio/intellij/gutter/ZLayerLineMarkerProvider.scala
new file mode 100644
index 00000000..f1f77823
--- /dev/null
+++ b/src/main/scala/zio/intellij/gutter/ZLayerLineMarkerProvider.scala
@@ -0,0 +1,54 @@
+package zio.intellij.gutter
+
+import com.intellij.codeInsight.daemon.{LineMarkerInfo, LineMarkerProvider}
+import com.intellij.openapi.editor.markup.GutterIconRenderer.Alignment
+import com.intellij.openapi.fileEditor.FileEditor
+import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiElement
+import com.intellij.ui.jcef.JBCefApp
+import com.intellij.util.Urls
+import org.jetbrains.plugins.scala.lang.psi.api.expr.{ScExpression, ScGenericCall, ScMethodCall}
+import zio.intellij.icons.LayersIcon
+import zio.intellij.inspections._
+import zio.intellij.inspections.macros.MacroGraphUtils
+import zio.intellij.inspections.zioMethods._
+
+import java.awt.event.MouseEvent
+
+final class ZLayerLineMarkerProvider extends LineMarkerProvider {
+ import zio.intellij.gutter.ZLayerLineMarkerProvider.createLineMarkerInfo
+
+ override def getLineMarkerInfo(element: PsiElement): LineMarkerInfo[_ <: PsiElement] = element match {
+ case `.inject`(base, layers @ _*) =>
+ createLineMarkerInfo(base, layers)
+ case ScMethodCall(partiallyApplied @ ScGenericCall(`ZLayer.makeLike`(_, _), _), layers) =>
+ createLineMarkerInfo(partiallyApplied, layers)
+ case _ => null
+ }
+
+}
+object ZLayerLineMarkerProvider {
+ def createLineMarkerInfo(element: ScExpression, layers: Seq[ScExpression]): LineMarkerInfo[PsiElement] =
+ new LineMarkerInfo(
+ element,
+ element.getTextRange,
+ LayersIcon,
+ (_: PsiElement) => "Show ZLayer build graph (Mermaid)",
+ (_: MouseEvent, _: PsiElement) =>
+ MacroGraphUtils.renderMermaid(element) match {
+ case Some(mermaid) =>
+ browse(element.getProject, mermaid): Unit
+ case None =>
+ },
+ Alignment.LEFT,
+ () => "Show ZLayer build graph"
+ )
+
+ def browse(project: Project, url: String): FileEditor = {
+ if (!JBCefApp.isSupported) throw new IllegalStateException("JCEF is not supported on this system")
+ val request: HTMLEditorProvider.Request = HTMLEditorProvider.Request.url(Urls.newFromEncoded(url).toExternalForm)
+ request.withQueryHandler(null)
+ HTMLEditorProvider.openEditor(project, "ZLayer Mermaid Diagram", request)
+ }
+}
diff --git a/src/main/scala/zio/intellij/gutter/package.scala b/src/main/scala/zio/intellij/gutter/package.scala
deleted file mode 100644
index 472cdee3..00000000
--- a/src/main/scala/zio/intellij/gutter/package.scala
+++ /dev/null
@@ -1,7 +0,0 @@
-package zio.intellij
-
-import javax.swing.Icon
-
-package object gutter {
- val fiberIcon: Icon = com.intellij.icons.AllIcons.Debugger.Threads
-}
diff --git a/src/main/scala/zio/intellij/icons/package.scala b/src/main/scala/zio/intellij/icons/package.scala
index ab73e331..d30e8e73 100644
--- a/src/main/scala/zio/intellij/icons/package.scala
+++ b/src/main/scala/zio/intellij/icons/package.scala
@@ -1,10 +1,8 @@
package zio.intellij
-import com.intellij.ui.IconManager
import javax.swing.Icon
package object icons {
- private def load(path: String): Icon = IconManager.getInstance.getIcon(path, getClass.getClassLoader)
-
- val SortByIdIcon: Icon = load("/icons/sortById.svg")
+ val FiberIcon: Icon = com.intellij.icons.AllIcons.Debugger.Threads
+ val LayersIcon: Icon = com.intellij.icons.ExpUiIcons.Toolwindow.Dependencies
}
diff --git a/src/main/scala/zio/intellij/inspections/macros/MacroGraphUtils.scala b/src/main/scala/zio/intellij/inspections/macros/MacroGraphUtils.scala
new file mode 100644
index 00000000..07110606
--- /dev/null
+++ b/src/main/scala/zio/intellij/inspections/macros/MacroGraphUtils.scala
@@ -0,0 +1,62 @@
+package zio.intellij.inspections.macros
+
+import org.jetbrains.plugins.scala.lang.psi.api.expr.ScExpression
+import zio.intellij.inspections.macros.LayerBuilder.{MermaidGraph, ZExpr}
+
+import java.nio.charset.StandardCharsets
+import java.util.Base64
+import scala.collection.mutable
+
+object MacroGraphUtils {
+
+ def renderMermaid(element: ScExpression): Option[String] = {
+ Option(element.getUserData(GraphDataKey)).map(renderMermaidLink)
+ }
+
+ private def renderMermaidLink(tree: LayerTree[ZExpr]) = {
+
+ def escapeString(string: String): String =
+ "\\\"" + string.replace("\"", """) + "\\\""
+
+ val map = tree
+ .map(expr => escapeString(expr.value.getText))
+ .fold[MermaidGraph](
+ z = MermaidGraph.empty,
+ value = MermaidGraph.make,
+ composeH = _ ++ _,
+ composeV = _ >>> _
+ )
+ .deps
+
+ val aliases = mutable.Map.empty[String, String]
+
+ def getAlias(name: String): String =
+ aliases.getOrElse(
+ name, {
+ val alias = s"L${aliases.size}"
+ aliases += name -> alias
+ s"$alias($name)"
+ }
+ )
+
+ val mermaidCode: String =
+ map.flatMap {
+ case (key, children) if children.isEmpty =>
+ List(getAlias(key))
+ case (key, children) =>
+ children.map { child =>
+ s"${getAlias(child)} --> ${getAlias(key)}"
+ }
+ }
+ .mkString("\\n")
+
+ val mermaidGraph =
+ s"""{"code":"graph BT\\n$mermaidCode","mermaid": "{\\"theme\\": \\"default\\"}"}"""
+
+ val encodedMermaidGraph: String =
+ new String(Base64.getEncoder.encode(mermaidGraph.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)
+
+ val mermaidLink = s"https://mermaid.live/view/#$encodedMermaidGraph"
+ mermaidLink
+ }
+}
diff --git a/src/main/scala/zio/intellij/inspections/macros/ProvideMacroInspection.scala b/src/main/scala/zio/intellij/inspections/macros/ProvideMacroInspection.scala
index 32367371..bbba8ab2 100644
--- a/src/main/scala/zio/intellij/inspections/macros/ProvideMacroInspection.scala
+++ b/src/main/scala/zio/intellij/inspections/macros/ProvideMacroInspection.scala
@@ -236,6 +236,7 @@ class ProvideMacroInspection extends LocalInspectionTool {
private def visitIssue(holder: ProblemsHolder, expr: ScExpression)(issue: ConstructionIssue): Unit =
issue match {
+ case UnsupportedIssue => ()
case error: ConstructionError => visitError(holder, expr)(error)
case warning: ConstructionWarning => visitWarning(holder, expr)(warning)
}
@@ -253,7 +254,7 @@ class ProvideMacroInspection extends LocalInspectionTool {
duplicates.foreach {
case (tpe, exprs) =>
exprs.foreach { expr =>
- holder.registerProblem(expr.value, ambigousLayersError(tpe, exprs), errorHighlight)
+ holder.registerProblem(expr.value, ambiguousLayersError(tpe, exprs), errorHighlight)
}
}
case CircularityError(circular) =>
@@ -311,7 +312,16 @@ final case class LayerBuilder(
typeToLayer: ZType => String
)(implicit pContext: ProjectContext, scalaFeatures: ScalaFeatures) {
- def tryBuild: Either[ConstructionIssue, Unit] = assertNoAmbiguity.flatMap(_ => tryBuildInternal)
+ // TODO find a better way!
+ def tryBuild(expr: ScExpression): Either[ConstructionIssue, Unit] = assertNoAmbiguity.flatMap(_ =>
+ layerTreeEither match {
+ case Left(buildErrors) => Left(graphToConstructionErrors(buildErrors))
+ case Right(tree) =>
+ // forgive me for I have side-effected :(
+ expr.putUserData(GraphDataKey, tree)
+ Right(warnUnused(tree))
+ }
+ )
private val target =
if (method.isProvideSomeShared) target0.filterNot(t => remainder.exists(_.isSubtypeOf(t)))
@@ -334,27 +344,21 @@ final case class LayerBuilder(
else Left(DuplicateLayersError(duplicates))
}
- private def tryBuildInternal: Either[ConstructionIssue, Unit] = {
+ def layerTreeEither: Either[::[GraphError], LayerTree[ZExpr]] = {
/**
* Build the layer tree. This represents the structure of a successfully
* constructed ZLayer that will build the target types. This, of course, may
* fail with one or more GraphErrors.
*/
- val layerTreeEither: Either[::[GraphError], LayerTree[ZExpr]] = {
- val nodes = providedLayerNodes ++ remainderNodes ++ sideEffectNodes
- val graph = Graph(nodes, _.isSubtypeOf(_))
- for {
- original <- graph.buildComplete(target)
- sideEffects <- graph.buildNodes(sideEffectNodes)
- } yield sideEffects ++ original
- }
+ val nodes = providedLayerNodes ++ remainderNodes ++ sideEffectNodes
+ val graph = Graph(nodes, _.isSubtypeOf(_))
- layerTreeEither match {
- case Left(buildErrors) => Left(graphToConstructionErrors(buildErrors))
- case Right(tree) => warnUnused(tree)
- }
+ for {
+ original <- graph.buildComplete(target)
+ sideEffects <- graph.buildNodes(sideEffectNodes)
+ } yield sideEffects ++ original
}
/**
@@ -449,6 +453,27 @@ final case class LayerBuilder(
object LayerBuilder {
+ final case class MermaidGraph(topLevel: List[String], deps: Map[String, List[String]]) {
+ def ++(that: MermaidGraph): MermaidGraph =
+ MermaidGraph(topLevel ++ that.topLevel, deps ++ that.deps)
+
+ def >>>(that: MermaidGraph): MermaidGraph = {
+ val newDeps =
+ that.deps.map {
+ case (key, values) =>
+ key -> (values ++ topLevel)
+ }
+ MermaidGraph(that.topLevel, deps ++ newDeps)
+ }
+ }
+
+ object MermaidGraph {
+ def empty: MermaidGraph = MermaidGraph(List.empty, Map.empty)
+
+ def make(string: String): MermaidGraph =
+ MermaidGraph(List(string), Map(string -> List.empty))
+ }
+
// version specific: making sure ScType <: Has[_]
def tryBuildZIO1(expr: ScExpression)(
target: Seq[ScType],
@@ -489,7 +514,7 @@ object LayerBuilder {
val remainder0 = remainder.toList.flatMap(toZType)
val providedLayerNodes0 = providedLayers.toList.flatMap(layerToNode)
- if (containsNothingAsRequirement(target, remainder, providedLayerNodes0)) Right(())
+ if (containsNothingAsRequirement(target, remainder, providedLayerNodes0)) Left(UnsupportedIssue)
else if (nonHasTypes.nonEmpty)
Left(NonHasTypesError(nonHasTypes.toSet))
else
@@ -500,7 +525,7 @@ object LayerBuilder {
sideEffectNodes = Nil,
method = method,
typeToLayer = tpe => s"_root_.zio.ZLayer.requires[$tpe]"
- ).tryBuild
+ ).tryBuild(expr)
}
// version-specific: taking care of Debug and side-effect layers
@@ -532,7 +557,7 @@ object LayerBuilder {
val (sideEffectNodes, providedLayerNodes) =
providedLayers.toList.flatMap(layerToNode).partition(_.outputs.exists(o => api.Unit.conforms(o.value)))
- if (containsNothingAsRequirement(target, remainder, providedLayerNodes)) Right(())
+ if (containsNothingAsRequirement(target, remainder, providedLayerNodes)) Left(UnsupportedIssue)
else
LayerBuilder(
target0 = target.toList.flatMap(ZType(_)),
@@ -541,7 +566,7 @@ object LayerBuilder {
sideEffectNodes = sideEffectNodes,
method = method,
typeToLayer = tpe => s"_root_.zio.ZLayer.environment[$tpe]"
- ).tryBuild
+ ).tryBuild(expr)
}
// Sometimes IntelliJ fails to infer actual type and uses `Nothing` instead.
@@ -603,6 +628,7 @@ object LayerBuilder {
}
sealed trait ConstructionIssue
+ case object UnsupportedIssue extends ConstructionIssue // in case of Scala 3 / IntelliJ inference issues
sealed trait ConstructionError extends ConstructionIssue
final case class DuplicateLayersError(duplicates: Map[ZType, Seq[ZExpr]]) extends ConstructionError
@@ -628,6 +654,9 @@ sealed abstract class LayerTree[+A] { self =>
def ++[A1 >: A](that: LayerTree[A1]): LayerTree[A1] =
if (self eq Empty) that else if (that eq Empty) self else ComposeH(self, that)
+ def map[B](f: A => B): LayerTree[B] =
+ fold[LayerTree[B]](Empty, a => Value(f(a)), ComposeH(_, _), ComposeV(_, _))
+
def fold[B](z: B, value: A => B, composeH: (B, B) => B, composeV: (B, B) => B): B = self match {
case Empty => z
case Value(value0) => value(value0)
@@ -638,7 +667,6 @@ sealed abstract class LayerTree[+A] { self =>
}
def toSet[A1 >: A]: Set[A1] = fold[Set[A1]](Set.empty[A1], Set(_), _ ++ _, _ ++ _)
-
}
object LayerTree {
@@ -756,7 +784,7 @@ object ErrorRendering {
List(header, topLevelString, transitiveStrings, footer).flatten.mkString(lineSeparator)
}
- def ambigousLayersError(tpe: ZType, providedBy: Seq[ZExpr]): String =
+ def ambiguousLayersError(tpe: ZType, providedBy: Seq[ZExpr]): String =
s"""Ambiguous layers! $tpe is provided by:
|${providedBy.mkString(lineSeparator)}""".stripMargin
@@ -764,9 +792,7 @@ object ErrorRendering {
s"""|Circular Dependency Detected
|A layer simultaneously requires and is required by another:
| "◉" $b
-
| "╰─◉" $a
-
| "╰─ ◉" $b""".stripMargin
def nonHasTypeError(types: Set[ZType]): String =
diff --git a/src/main/scala/zio/intellij/inspections/macros/package.scala b/src/main/scala/zio/intellij/inspections/macros/package.scala
new file mode 100644
index 00000000..429c427f
--- /dev/null
+++ b/src/main/scala/zio/intellij/inspections/macros/package.scala
@@ -0,0 +1,8 @@
+package zio.intellij.inspections
+
+import com.intellij.openapi.util.Key
+import zio.intellij.inspections.macros.LayerBuilder.ZExpr
+
+package object macros {
+ val GraphDataKey: Key[LayerTree[ZExpr]] = Key.create[LayerTree[ZExpr]]("ZLayerGraphData")
+}
diff --git a/src/main/scala/zio/intellij/inspections/package.scala b/src/main/scala/zio/intellij/inspections/package.scala
index 9730e384..1422fbd8 100644
--- a/src/main/scala/zio/intellij/inspections/package.scala
+++ b/src/main/scala/zio/intellij/inspections/package.scala
@@ -66,6 +66,8 @@ package object inspections {
val `.forkAs`: Qualified = invocation("forkAs").from(zioLikePackages)
val `.forkOn`: Qualified = invocation("forkOn").from(zioLikePackages)
val `.forkWithErrorHandler`: Qualified = invocation("forkWithErrorHandler").from(zioLikePackages)
+ val `.toLayer`: Qualified = invocation("toLayer").from(zioLikePackages)
+ val `.toLayerMany`: Qualified = invocation("toLayerMany").from(zioLikePackages)
}
object streamMethods {
diff --git a/src/main/scala/zio/intellij/utils/package.scala b/src/main/scala/zio/intellij/utils/package.scala
index 7051d778..261f2f1c 100644
--- a/src/main/scala/zio/intellij/utils/package.scala
+++ b/src/main/scala/zio/intellij/utils/package.scala
@@ -27,14 +27,7 @@ import org.jetbrains.plugins.scala.lang.psi.types.result.Typeable
import org.jetbrains.plugins.scala.lang.refactoring.ScTypePresentationExt
import org.jetbrains.plugins.scala.lang.refactoring.util.ScalaNamesUtil
import org.jetbrains.plugins.scala.project.settings.ScalaCompilerConfiguration
-import org.jetbrains.plugins.scala.project.{
- LibraryExt,
- ModuleExt,
- ProjectContext,
- ProjectExt,
- ProjectPsiElementExt,
- ScalaLanguageLevel
-}
+import org.jetbrains.plugins.scala.project.{LibraryExt, ModuleExt, ProjectContext, ProjectExt, ProjectPsiElementExt, ScalaLanguageLevel}
import org.jetbrains.sbt.SbtUtil
import org.jetbrains.sbt.SbtUtil.getDefaultLauncher
import org.jetbrains.sbt.project.SbtExternalSystemManager
@@ -309,14 +302,6 @@ package object utils {
def isZio2: Boolean = element.module.exists(_.isZio2)
}
- implicit class TraverseAtHome[A](private val list: List[A]) extends AnyVal {
- def map2[A, B, C](oa: Option[A], ob: Option[B])(f: (A, B) => C): Option[C] =
- oa.flatMap(a => ob.map(b => f(a, b)))
-
- def traverse[B](f: A => Option[B]): Option[List[B]] =
- list.foldRight[Option[List[B]]](Some(Nil))((h, t) => map2(f(h), t)(_ :: _))
- }
-
implicit final class ListSyntax[A](private val list: List[A]) extends AnyVal {
// Similar to .minBy, but returns all minimal elements from original list