Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ZLayer visualization #473

Draft
wants to merge 1 commit into
base: idea241.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@
<codeInsight.lineMarkerProvider implementationClass="zio.intellij.gutter.ForkedCodeLineMarkerProvider"
language="Scala"/>

<codeInsight.lineMarkerProvider implementationClass="zio.intellij.gutter.ZLayerLineMarkerProvider"
language="Scala"/>

<referencesSearch implementation="zio.intellij.searchers.ZioAccessorUsagesSearcher"/>

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



<notificationGroup id="Test Runner Download" displayType="BALLOON" isLogByDefault="true"/>
<notificationGroup id="Test Runner Download Error" displayType="STICKY_BALLOON" isLogByDefault="true"/>

Expand Down
22 changes: 0 additions & 22 deletions src/main/resources/icons/sortById.svg

This file was deleted.

22 changes: 0 additions & 22 deletions src/main/resources/icons/sortById_dark.svg

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand Down Expand Up @@ -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,
Expand Down
54 changes: 54 additions & 0 deletions src/main/scala/zio/intellij/gutter/ZLayerLineMarkerProvider.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
7 changes: 0 additions & 7 deletions src/main/scala/zio/intellij/gutter/package.scala

This file was deleted.

6 changes: 2 additions & 4 deletions src/main/scala/zio/intellij/icons/package.scala
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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("\"", "&quot") + "\\\""

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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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) =>
Expand Down Expand Up @@ -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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgive me @myazinn for I have sinned :D
(I need to recover the LayerTree somehow... any ideas are most welcome!)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feature look too good for me to grumble on side-effects 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This desperately needs a State, but screw it :)
My initial approach was to duplicate the inspection logic inside the gutter, but I decided against it. Passing data in the element's cache is good enough for now.

Just need to tweak the gutter menu.
Thanks! :)

Right(warnUnused(tree))
}
)

private val target =
if (method.isProvideSomeShared) target0.filterNot(t => remainder.exists(_.isSubtypeOf(t)))
Expand All @@ -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
}

/**
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(_)),
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -756,17 +784,15 @@ 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

def circularityError(a: ZExpr, b: ZExpr): String =
s"""|Circular Dependency Detected
|A layer simultaneously requires and is required by another:
| "◉" $b

| "╰─◉" $a

| "╰─ ◉" $b""".stripMargin

def nonHasTypeError(types: Set[ZType]): String =
Expand Down
Loading
Loading