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 @@ - - - - - - - - - i - - - d - - 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 @@ - - - - - - - - - i - - - d - - 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