Skip to content

Commit b184437

Browse files
committed
ZLayer gutter icon + generate mermaid graph
1 parent f703c61 commit b184437

12 files changed

+183
-97
lines changed

src/main/resources/META-INF/plugin.xml

+3-2
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,9 @@
321321
<codeInsight.lineMarkerProvider implementationClass="zio.intellij.gutter.ForkedCodeLineMarkerProvider"
322322
language="Scala"/>
323323

324+
<codeInsight.lineMarkerProvider implementationClass="zio.intellij.gutter.ZLayerLineMarkerProvider"
325+
language="Scala"/>
326+
324327
<referencesSearch implementation="zio.intellij.searchers.ZioAccessorUsagesSearcher"/>
325328

326329
<!-- test support -->
@@ -333,8 +336,6 @@
333336
<runLineMarkerContributor implementationClass="zio.intellij.testsupport.ZTestRunLineMarkerProvider"
334337
language="Scala" order="first"/>
335338

336-
337-
338339
<notificationGroup id="Test Runner Download" displayType="BALLOON" isLogByDefault="true"/>
339340
<notificationGroup id="Test Runner Download Error" displayType="STICKY_BALLOON" isLogByDefault="true"/>
340341

src/main/resources/icons/sortById.svg

-22
This file was deleted.

src/main/resources/icons/sortById_dark.svg

-22
This file was deleted.

src/main/scala/zio/intellij/gutter/ForkedCodeLineMarkerProvider.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.intellij.openapi.editor.markup.GutterIconRenderer.Alignment
55
import com.intellij.psi.PsiElement
66
import org.jetbrains.plugins.scala.lang.lexer.ScalaTokenTypes
77
import org.jetbrains.plugins.scala.lang.psi.api.expr.ScReferenceExpression
8+
import zio.intellij.icons.FiberIcon
89
import zio.intellij.inspections._
910
import zio.intellij.inspections.zioMethods._
1011

@@ -41,7 +42,7 @@ object ForkedCodeLineMarkerProvider {
4142
new LineMarkerInfo(
4243
element,
4344
element.getTextRange,
44-
fiberIcon,
45+
FiberIcon,
4546
(_: PsiElement) => s"Effect is explicitly forked via '${element.getText}'",
4647
null, // the handler executed when the gutter icon is clicked
4748
Alignment.LEFT,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package zio.intellij.gutter
2+
3+
import com.intellij.codeInsight.daemon.{LineMarkerInfo, LineMarkerProvider}
4+
import com.intellij.openapi.editor.markup.GutterIconRenderer.Alignment
5+
import com.intellij.openapi.fileEditor.FileEditor
6+
import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider
7+
import com.intellij.openapi.project.Project
8+
import com.intellij.psi.PsiElement
9+
import com.intellij.ui.jcef.JBCefApp
10+
import com.intellij.util.Urls
11+
import org.jetbrains.plugins.scala.lang.psi.api.expr.{ScExpression, ScGenericCall, ScMethodCall}
12+
import zio.intellij.icons.LayersIcon
13+
import zio.intellij.inspections._
14+
import zio.intellij.inspections.macros.MacroGraphUtils
15+
import zio.intellij.inspections.zioMethods._
16+
17+
import java.awt.event.MouseEvent
18+
19+
final class ZLayerLineMarkerProvider extends LineMarkerProvider {
20+
import zio.intellij.gutter.ZLayerLineMarkerProvider.createLineMarkerInfo
21+
22+
override def getLineMarkerInfo(element: PsiElement): LineMarkerInfo[_ <: PsiElement] = element match {
23+
case `.inject`(base, layers @ _*) =>
24+
createLineMarkerInfo(base, layers)
25+
case ScMethodCall(partiallyApplied @ ScGenericCall(`ZLayer.makeLike`(_, _), _), layers) =>
26+
createLineMarkerInfo(partiallyApplied, layers)
27+
case _ => null
28+
}
29+
30+
}
31+
object ZLayerLineMarkerProvider {
32+
def createLineMarkerInfo(element: ScExpression, layers: Seq[ScExpression]): LineMarkerInfo[PsiElement] =
33+
new LineMarkerInfo(
34+
element,
35+
element.getTextRange,
36+
LayersIcon,
37+
(_: PsiElement) => "Show ZLayer build graph (Mermaid)",
38+
(_: MouseEvent, _: PsiElement) =>
39+
MacroGraphUtils.renderMermaid(element) match {
40+
case Some(mermaid) =>
41+
browse(element.getProject, mermaid): Unit
42+
case None =>
43+
},
44+
Alignment.LEFT,
45+
() => "Show ZLayer build graph"
46+
)
47+
48+
def browse(project: Project, url: String): FileEditor = {
49+
if (!JBCefApp.isSupported) throw new IllegalStateException("JCEF is not supported on this system")
50+
val request: HTMLEditorProvider.Request = HTMLEditorProvider.Request.url(Urls.newFromEncoded(url).toExternalForm)
51+
request.withQueryHandler(null)
52+
HTMLEditorProvider.openEditor(project, "ZLayer Mermaid Diagram", request)
53+
}
54+
}

src/main/scala/zio/intellij/gutter/package.scala

-7
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package zio.intellij
22

3-
import com.intellij.ui.IconManager
43
import javax.swing.Icon
54

65
package object icons {
7-
private def load(path: String): Icon = IconManager.getInstance.getIcon(path, getClass.getClassLoader)
8-
9-
val SortByIdIcon: Icon = load("/icons/sortById.svg")
6+
val FiberIcon: Icon = com.intellij.icons.AllIcons.Debugger.Threads
7+
val LayersIcon: Icon = com.intellij.icons.ExpUiIcons.Toolwindow.Dependencies
108
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package zio.intellij.inspections.macros
2+
3+
import org.jetbrains.plugins.scala.lang.psi.api.expr.ScExpression
4+
import zio.intellij.inspections.macros.LayerBuilder.{MermaidGraph, ZExpr}
5+
6+
import java.nio.charset.StandardCharsets
7+
import java.util.Base64
8+
import scala.collection.mutable
9+
10+
object MacroGraphUtils {
11+
12+
def renderMermaid(element: ScExpression): Option[String] = {
13+
Option(element.getUserData(GraphDataKey)).map(renderMermaidLink)
14+
}
15+
16+
private def renderMermaidLink(tree: LayerTree[ZExpr]) = {
17+
18+
def escapeString(string: String): String =
19+
"\\\"" + string.replace("\"", "&quot") + "\\\""
20+
21+
val map = tree
22+
.map(expr => escapeString(expr.value.getText))
23+
.fold[MermaidGraph](
24+
z = MermaidGraph.empty,
25+
value = MermaidGraph.make,
26+
composeH = _ ++ _,
27+
composeV = _ >>> _
28+
)
29+
.deps
30+
31+
val aliases = mutable.Map.empty[String, String]
32+
33+
def getAlias(name: String): String =
34+
aliases.getOrElse(
35+
name, {
36+
val alias = s"L${aliases.size}"
37+
aliases += name -> alias
38+
s"$alias($name)"
39+
}
40+
)
41+
42+
val mermaidCode: String =
43+
map.flatMap {
44+
case (key, children) if children.isEmpty =>
45+
List(getAlias(key))
46+
case (key, children) =>
47+
children.map { child =>
48+
s"${getAlias(child)} --> ${getAlias(key)}"
49+
}
50+
}
51+
.mkString("\\n")
52+
53+
val mermaidGraph =
54+
s"""{"code":"graph BT\\n$mermaidCode","mermaid": "{\\"theme\\": \\"default\\"}"}"""
55+
56+
val encodedMermaidGraph: String =
57+
new String(Base64.getEncoder.encode(mermaidGraph.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)
58+
59+
val mermaidLink = s"https://mermaid.live/view/#$encodedMermaidGraph"
60+
mermaidLink
61+
}
62+
}

src/main/scala/zio/intellij/inspections/macros/ProvideMacroInspection.scala

+49-23
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ class ProvideMacroInspection extends LocalInspectionTool {
236236

237237
private def visitIssue(holder: ProblemsHolder, expr: ScExpression)(issue: ConstructionIssue): Unit =
238238
issue match {
239+
case UnsupportedIssue => ()
239240
case error: ConstructionError => visitError(holder, expr)(error)
240241
case warning: ConstructionWarning => visitWarning(holder, expr)(warning)
241242
}
@@ -253,7 +254,7 @@ class ProvideMacroInspection extends LocalInspectionTool {
253254
duplicates.foreach {
254255
case (tpe, exprs) =>
255256
exprs.foreach { expr =>
256-
holder.registerProblem(expr.value, ambigousLayersError(tpe, exprs), errorHighlight)
257+
holder.registerProblem(expr.value, ambiguousLayersError(tpe, exprs), errorHighlight)
257258
}
258259
}
259260
case CircularityError(circular) =>
@@ -311,7 +312,16 @@ final case class LayerBuilder(
311312
typeToLayer: ZType => String
312313
)(implicit pContext: ProjectContext, scalaFeatures: ScalaFeatures) {
313314

314-
def tryBuild: Either[ConstructionIssue, Unit] = assertNoAmbiguity.flatMap(_ => tryBuildInternal)
315+
// TODO find a better way!
316+
def tryBuild(expr: ScExpression): Either[ConstructionIssue, Unit] = assertNoAmbiguity.flatMap(_ =>
317+
layerTreeEither match {
318+
case Left(buildErrors) => Left(graphToConstructionErrors(buildErrors))
319+
case Right(tree) =>
320+
// forgive me for I have side-effected :(
321+
expr.putUserData(GraphDataKey, tree)
322+
Right(warnUnused(tree))
323+
}
324+
)
315325

316326
private val target =
317327
if (method.isProvideSomeShared) target0.filterNot(t => remainder.exists(_.isSubtypeOf(t)))
@@ -334,27 +344,21 @@ final case class LayerBuilder(
334344
else Left(DuplicateLayersError(duplicates))
335345
}
336346

337-
private def tryBuildInternal: Either[ConstructionIssue, Unit] = {
347+
def layerTreeEither: Either[::[GraphError], LayerTree[ZExpr]] = {
338348

339349
/**
340350
* Build the layer tree. This represents the structure of a successfully
341351
* constructed ZLayer that will build the target types. This, of course, may
342352
* fail with one or more GraphErrors.
343353
*/
344-
val layerTreeEither: Either[::[GraphError], LayerTree[ZExpr]] = {
345-
val nodes = providedLayerNodes ++ remainderNodes ++ sideEffectNodes
346-
val graph = Graph(nodes, _.isSubtypeOf(_))
347354

348-
for {
349-
original <- graph.buildComplete(target)
350-
sideEffects <- graph.buildNodes(sideEffectNodes)
351-
} yield sideEffects ++ original
352-
}
355+
val nodes = providedLayerNodes ++ remainderNodes ++ sideEffectNodes
356+
val graph = Graph(nodes, _.isSubtypeOf(_))
353357

354-
layerTreeEither match {
355-
case Left(buildErrors) => Left(graphToConstructionErrors(buildErrors))
356-
case Right(tree) => warnUnused(tree)
357-
}
358+
for {
359+
original <- graph.buildComplete(target)
360+
sideEffects <- graph.buildNodes(sideEffectNodes)
361+
} yield sideEffects ++ original
358362
}
359363

360364
/**
@@ -449,6 +453,27 @@ final case class LayerBuilder(
449453

450454
object LayerBuilder {
451455

456+
final case class MermaidGraph(topLevel: List[String], deps: Map[String, List[String]]) {
457+
def ++(that: MermaidGraph): MermaidGraph =
458+
MermaidGraph(topLevel ++ that.topLevel, deps ++ that.deps)
459+
460+
def >>>(that: MermaidGraph): MermaidGraph = {
461+
val newDeps =
462+
that.deps.map {
463+
case (key, values) =>
464+
key -> (values ++ topLevel)
465+
}
466+
MermaidGraph(that.topLevel, deps ++ newDeps)
467+
}
468+
}
469+
470+
object MermaidGraph {
471+
def empty: MermaidGraph = MermaidGraph(List.empty, Map.empty)
472+
473+
def make(string: String): MermaidGraph =
474+
MermaidGraph(List(string), Map(string -> List.empty))
475+
}
476+
452477
// version specific: making sure ScType <: Has[_]
453478
def tryBuildZIO1(expr: ScExpression)(
454479
target: Seq[ScType],
@@ -489,7 +514,7 @@ object LayerBuilder {
489514
val remainder0 = remainder.toList.flatMap(toZType)
490515
val providedLayerNodes0 = providedLayers.toList.flatMap(layerToNode)
491516

492-
if (containsNothingAsRequirement(target, remainder, providedLayerNodes0)) Right(())
517+
if (containsNothingAsRequirement(target, remainder, providedLayerNodes0)) Left(UnsupportedIssue)
493518
else if (nonHasTypes.nonEmpty)
494519
Left(NonHasTypesError(nonHasTypes.toSet))
495520
else
@@ -500,7 +525,7 @@ object LayerBuilder {
500525
sideEffectNodes = Nil,
501526
method = method,
502527
typeToLayer = tpe => s"_root_.zio.ZLayer.requires[$tpe]"
503-
).tryBuild
528+
).tryBuild(expr)
504529
}
505530

506531
// version-specific: taking care of Debug and side-effect layers
@@ -532,7 +557,7 @@ object LayerBuilder {
532557
val (sideEffectNodes, providedLayerNodes) =
533558
providedLayers.toList.flatMap(layerToNode).partition(_.outputs.exists(o => api.Unit.conforms(o.value)))
534559

535-
if (containsNothingAsRequirement(target, remainder, providedLayerNodes)) Right(())
560+
if (containsNothingAsRequirement(target, remainder, providedLayerNodes)) Left(UnsupportedIssue)
536561
else
537562
LayerBuilder(
538563
target0 = target.toList.flatMap(ZType(_)),
@@ -541,7 +566,7 @@ object LayerBuilder {
541566
sideEffectNodes = sideEffectNodes,
542567
method = method,
543568
typeToLayer = tpe => s"_root_.zio.ZLayer.environment[$tpe]"
544-
).tryBuild
569+
).tryBuild(expr)
545570
}
546571

547572
// Sometimes IntelliJ fails to infer actual type and uses `Nothing` instead.
@@ -603,6 +628,7 @@ object LayerBuilder {
603628
}
604629

605630
sealed trait ConstructionIssue
631+
case object UnsupportedIssue extends ConstructionIssue // in case of Scala 3 / IntelliJ inference issues
606632

607633
sealed trait ConstructionError extends ConstructionIssue
608634
final case class DuplicateLayersError(duplicates: Map[ZType, Seq[ZExpr]]) extends ConstructionError
@@ -628,6 +654,9 @@ sealed abstract class LayerTree[+A] { self =>
628654
def ++[A1 >: A](that: LayerTree[A1]): LayerTree[A1] =
629655
if (self eq Empty) that else if (that eq Empty) self else ComposeH(self, that)
630656

657+
def map[B](f: A => B): LayerTree[B] =
658+
fold[LayerTree[B]](Empty, a => Value(f(a)), ComposeH(_, _), ComposeV(_, _))
659+
631660
def fold[B](z: B, value: A => B, composeH: (B, B) => B, composeV: (B, B) => B): B = self match {
632661
case Empty => z
633662
case Value(value0) => value(value0)
@@ -638,7 +667,6 @@ sealed abstract class LayerTree[+A] { self =>
638667
}
639668

640669
def toSet[A1 >: A]: Set[A1] = fold[Set[A1]](Set.empty[A1], Set(_), _ ++ _, _ ++ _)
641-
642670
}
643671

644672
object LayerTree {
@@ -756,17 +784,15 @@ object ErrorRendering {
756784
List(header, topLevelString, transitiveStrings, footer).flatten.mkString(lineSeparator)
757785
}
758786

759-
def ambigousLayersError(tpe: ZType, providedBy: Seq[ZExpr]): String =
787+
def ambiguousLayersError(tpe: ZType, providedBy: Seq[ZExpr]): String =
760788
s"""Ambiguous layers! $tpe is provided by:
761789
|${providedBy.mkString(lineSeparator)}""".stripMargin
762790

763791
def circularityError(a: ZExpr, b: ZExpr): String =
764792
s"""|Circular Dependency Detected
765793
|A layer simultaneously requires and is required by another:
766794
| "◉" $b
767-
768795
| "╰─◉" $a
769-
770796
| "╰─ ◉" $b""".stripMargin
771797

772798
def nonHasTypeError(types: Set[ZType]): String =

0 commit comments

Comments
 (0)