Skip to content
Merged
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: 5 additions & 0 deletions .changeset/jetbrains-mermaid-all-diagrams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": minor
---

Render all Mermaid diagram types natively in JetBrains chat: class, state, ER, gantt, pie, user journey, quadrant, requirement, git graph, C4, mindmap, timeline, sankey, XY chart, block, packet, kanban, architecture, radar, and treemap now join flowcharts and sequence diagrams.
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,61 @@ internal data class Size(val w: Double, val h: Double) {

internal enum class Role { Surface, Border, Text, Muted, Accent, Note, Cluster, Line }

internal enum class Head { None, Arrow, Open, Cross, Dot }
internal enum class Head { None, Arrow, Open, Cross, Dot, Triangle, Diamond, DiamondFilled, Crow, Bar, CircleOpen }

internal enum class Anchor { TopLeft, Top, TopRight, Left, Center, Right, BottomLeft, Bottom, BottomRight }

@Serializable
internal sealed interface Mark {
@Serializable
data class Box(val rect: Rect, val arc: Double, val fill: Role?, val line: Role?, val dash: Boolean = false) : Mark {
override fun toString() = "box $rect arc=${fmt(arc)} fill=${fill.name()} line=${line.name()} dash=$dash"
data class Box(
val rect: Rect,
val arc: Double,
val fill: Role?,
val line: Role?,
val dash: Boolean = false,
val tone: Int? = null,
val soft: Boolean = false,
) : Mark {
override fun toString() = "box $rect arc=${fmt(arc)} fill=${fill.name()} line=${line.name()} dash=$dash${paint(tone, soft)}"
}

@Serializable
data class Oval(val rect: Rect, val fill: Role?, val line: Role?) : Mark {
override fun toString() = "oval $rect fill=${fill.name()} line=${line.name()}"
data class Oval(
val rect: Rect,
val fill: Role?,
val line: Role?,
val tone: Int? = null,
val soft: Boolean = false,
) : Mark {
override fun toString() = "oval $rect fill=${fill.name()} line=${line.name()}${paint(tone, soft)}"
}

@Serializable
data class Poly(val points: List<Pt>, val fill: Role?, val line: Role?) : Mark {
override fun toString() = "poly ${points.joinToString(" ")} fill=${fill.name()} line=${line.name()}"
data class Poly(
val points: List<Pt>,
val fill: Role?,
val line: Role?,
val tone: Int? = null,
val soft: Boolean = false,
) : Mark {
override fun toString() = "poly ${points.joinToString(" ")} fill=${fill.name()} line=${line.name()}${paint(tone, soft)}"
}

/** A filled circle wedge. [start] and [sweep] are degrees in AWT arc space: 0° at 3 o'clock, counterclockwise positive. */
@Serializable
data class Sector(
val at: Pt,
val r: Double,
val start: Double,
val sweep: Double,
val fill: Role?,
val line: Role?,
val tone: Int? = null,
val soft: Boolean = false,
) : Mark {
override fun toString() =
"sector at=$at r=${fmt(r)} start=${fmt(start)} sweep=${fmt(sweep)} fill=${fill.name()} line=${line.name()}${paint(tone, soft)}"
}

@Serializable
Expand All @@ -50,8 +86,11 @@ internal sealed interface Mark {
val thick: Boolean = false,
val head: Head = Head.None,
val tail: Head = Head.None,
val tone: Int? = null,
val soft: Boolean = false,
) : Mark {
override fun toString() = "edge ${points.joinToString(" ")} role=$role dash=$dash thick=$thick head=$head tail=$tail"
override fun toString() =
"edge ${points.joinToString(" ")} role=$role dash=$dash thick=$thick head=$head tail=$tail${paint(tone, soft)}"
}

@Serializable
Expand Down Expand Up @@ -81,6 +120,12 @@ internal data class Scene(@SerialName("diagram") val type: Type, val marks: List

private fun Role?.name() = this?.name ?: "-"

/** Tone/soft only appear when set so existing snapshots stay byte-stable. */
private fun paint(tone: Int?, soft: Boolean) = buildString {
if (tone != null) append(" tone=$tone")
if (soft) append(" soft")
}

internal fun fmt(value: Double): String = value.roundToInt().toString()

private fun quote(value: String) = buildString {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ internal data class Palette(
val line: Color,
val font: Font,
val bold: Font,
/** Categorical series colors for charts; falls back to [accent] when empty. */
val tones: List<Color> = emptyList(),
) {
fun color(role: Role): Color = when (role) {
Role.Surface -> surface
Expand All @@ -25,4 +27,9 @@ internal data class Palette(
Role.Cluster -> cluster
Role.Line -> line
}

fun tone(idx: Int): Color {
if (tones.isEmpty()) return accent
return tones[Math.floorMod(idx, tones.size)]
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package ai.kilocode.client.ui.diagram

import java.awt.BasicStroke
import java.awt.Color
import java.awt.Graphics2D
import java.awt.RenderingHints
import java.awt.Shape
import java.awt.geom.Arc2D
import java.awt.geom.Ellipse2D
import java.awt.geom.Line2D
import java.awt.geom.Path2D
Expand All @@ -20,6 +23,7 @@ internal object ScenePainter : Painter {
private const val HEAD = 10.0
private const val DOT = 4.0
private const val CROSS = 5.0
private const val SOFT = 96

override fun accepts(art: Art) = art is Scene

Expand All @@ -41,17 +45,26 @@ internal object ScenePainter : Painter {
is Mark.Box -> box(g, mark, palette)
is Mark.Oval -> oval(g, mark, palette)
is Mark.Poly -> poly(g, mark, palette)
is Mark.Sector -> sector(g, mark, palette)
is Mark.Edge -> edge(g, mark, palette)
is Mark.Text -> text(g, mark, palette)
is Mark.Group -> mark.marks.forEach { draw(g, it, palette) }
}
}

/** A [tone] wins over the role fill; [soft] applies a fixed translucency for stacked chart fills. */
private fun fill(palette: Palette, role: Role?, tone: Int?, soft: Boolean): Color? {
val base = if (tone != null) palette.tone(tone) else role?.let(palette::color)
if (base == null) return null
if (!soft) return base
return Color(base.red, base.green, base.blue, SOFT)
}

private fun box(g: Graphics2D, mark: Mark.Box, palette: Palette) {
val rect = mark.rect
val shape = RoundRectangle2D.Double(rect.x, rect.y, rect.w, rect.h, mark.arc, mark.arc)
mark.fill?.let {
g.color = palette.color(it)
fill(palette, mark.fill, mark.tone, mark.soft)?.let {
g.color = it
g.fill(shape)
}
mark.line?.let {
Expand All @@ -64,8 +77,8 @@ internal object ScenePainter : Painter {
private fun oval(g: Graphics2D, mark: Mark.Oval, palette: Palette) {
val rect = mark.rect
val shape = Ellipse2D.Double(rect.x, rect.y, rect.w, rect.h)
mark.fill?.let {
g.color = palette.color(it)
fill(palette, mark.fill, mark.tone, mark.soft)?.let {
g.color = it
g.fill(shape)
}
mark.line?.let {
Expand All @@ -77,8 +90,29 @@ internal object ScenePainter : Painter {

private fun poly(g: Graphics2D, mark: Mark.Poly, palette: Palette) {
val shape = path(mark.points, true)
mark.fill?.let {
fill(palette, mark.fill, mark.tone, mark.soft)?.let {
g.color = it
g.fill(shape)
}
mark.line?.let {
g.color = palette.color(it)
g.stroke = stroke()
g.draw(shape)
}
}

private fun sector(g: Graphics2D, mark: Mark.Sector, palette: Palette) {
val shape = Arc2D.Double(
mark.at.x - mark.r,
mark.at.y - mark.r,
mark.r * 2,
mark.r * 2,
mark.start,
mark.sweep,
Arc2D.PIE,
)
fill(palette, mark.fill, mark.tone, mark.soft)?.let {
g.color = it
g.fill(shape)
}
mark.line?.let {
Expand All @@ -90,11 +124,14 @@ internal object ScenePainter : Painter {

private fun edge(g: Graphics2D, mark: Mark.Edge, palette: Palette) {
if (mark.points.size < 2) return
g.color = palette.color(mark.role)
g.color = fill(palette, mark.role, mark.tone, mark.soft) ?: palette.color(mark.role)
g.stroke = stroke(mark.dash, mark.thick)
g.draw(path(mark.points, false))
head(g, mark.points[mark.points.lastIndex - 1], mark.points.last(), mark.head)
head(g, mark.points[1], mark.points.first(), mark.tail)
// Outline heads keep the line width but never the dash: a dashed triangle or crow's foot reads
// as a broken glyph on realization arrows and dashed ER relations.
g.stroke = stroke(thick = mark.thick)
head(g, palette, mark.points[mark.points.lastIndex - 1], mark.points.last(), mark.head)
Comment thread
kirillk marked this conversation as resolved.
head(g, palette, mark.points[1], mark.points.first(), mark.tail)
}

private fun text(g: Graphics2D, mark: Mark.Text, palette: Palette) {
Expand All @@ -116,21 +153,33 @@ internal object ScenePainter : Painter {
g.drawString(mark.text, x.toFloat(), y.toFloat())
}

private fun head(g: Graphics2D, from: Pt, to: Pt, head: Head) {
private fun head(g: Graphics2D, palette: Palette, from: Pt, to: Pt, head: Head) {
if (head == Head.None) return
val angle = atan2(to.y - from.y, to.x - from.x)
when (head) {
Head.Arrow -> {
val p = arrow(to, angle)
g.fill(p)
}
Head.Arrow -> g.fill(arrow(to, angle))
Head.Open -> g.draw(arrow(to, angle))
Head.Cross -> cross(g, to, angle)
Head.Dot -> g.fill(Ellipse2D.Double(to.x - DOT, to.y - DOT, DOT * 2, DOT * 2))
Head.Triangle -> hollow(g, palette, triangle(to, angle))
Head.Diamond -> hollow(g, palette, diamond(to, angle))
Head.DiamondFilled -> g.fill(diamond(to, angle))
Head.Crow -> crow(g, to, angle)
Head.Bar -> bar(g, to, angle)
Head.CircleOpen -> hollow(g, palette, Ellipse2D.Double(to.x - DOT, to.y - DOT, DOT * 2, DOT * 2))
Head.None -> Unit
}
}

/** UML-style hollow heads: surface fill so the line underneath does not show through, then outline. */
private fun hollow(g: Graphics2D, palette: Palette, shape: Shape) {
val color = g.color
g.color = palette.surface
g.fill(shape)
g.color = color
g.draw(shape)
}

private fun arrow(to: Pt, angle: Double): Path2D {
val left = point(to, angle + PI * 0.82, HEAD)
val right = point(to, angle - PI * 0.82, HEAD)
Expand All @@ -142,6 +191,48 @@ internal object ScenePainter : Painter {
}
}

private fun triangle(to: Pt, angle: Double): Path2D {
val left = point(to, angle + PI * 0.86, HEAD * 1.4)
val right = point(to, angle - PI * 0.86, HEAD * 1.4)
return Path2D.Double().apply {
moveTo(to.x, to.y)
lineTo(left.x, left.y)
lineTo(right.x, right.y)
closePath()
}
}

private fun diamond(to: Pt, angle: Double): Path2D {
val mid = point(to, angle + PI, HEAD)
val back = point(to, angle + PI, HEAD * 2)
val left = point(mid, angle + PI / 2, HEAD / 2)
val right = point(mid, angle - PI / 2, HEAD / 2)
return Path2D.Double().apply {
moveTo(to.x, to.y)
lineTo(left.x, left.y)
lineTo(back.x, back.y)
lineTo(right.x, right.y)
closePath()
}
}

/** Crow's foot: three prongs spreading back from the endpoint toward the line. */
private fun crow(g: Graphics2D, to: Pt, angle: Double) {
val root = point(to, angle + PI, HEAD)
val left = point(to, angle + PI / 2, HEAD / 2)
val right = point(to, angle - PI / 2, HEAD / 2)
g.draw(Line2D.Double(root.x, root.y, left.x, left.y))
g.draw(Line2D.Double(root.x, root.y, right.x, right.y))
g.draw(Line2D.Double(root.x, root.y, to.x, to.y))
}

private fun bar(g: Graphics2D, to: Pt, angle: Double) {
val mid = point(to, angle + PI, HEAD / 2)
val left = point(mid, angle + PI / 2, HEAD / 2)
val right = point(mid, angle - PI / 2, HEAD / 2)
g.draw(Line2D.Double(left.x, left.y, right.x, right.y))
}

private fun cross(g: Graphics2D, to: Pt, angle: Double) {
val a = point(to, angle + PI / 4.0, CROSS)
val b = point(to, angle + PI + PI / 4.0, CROSS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ internal enum class Type {
Er,
Gantt,
Pie,
Journey,
Quadrant,
Requirement,
Git,
C4,
Mindmap,
Timeline,
Sankey,
XyChart,
Block,
Packet,
Kanban,
Architecture,
Radar,
Treemap,
Unknown;

companion object {
Expand All @@ -24,7 +39,8 @@ internal enum class Type {

fun of(clean: Clean): Type {
val head = clean.lines.firstOrNull { it.text.isNotBlank() }?.text?.trim() ?: return Unknown
val token = head.takeWhile { !it.isWhitespace() }.lowercase()
// `gitGraph LR:` and `gitGraph:` keep a trailing colon on the keyword itself.
val token = head.takeWhile { !it.isWhitespace() }.trimEnd(':').lowercase()
return when (token) {
"graph", "flowchart" -> Flowchart
"sequencediagram" -> Sequence
Expand All @@ -33,6 +49,21 @@ internal enum class Type {
"erdiagram" -> Er
"gantt" -> Gantt
"pie" -> Pie
"journey" -> Journey
"quadrantchart" -> Quadrant
"requirementdiagram" -> Requirement
"gitgraph" -> Git
"c4context", "c4container", "c4component", "c4dynamic", "c4deployment" -> C4
"mindmap" -> Mindmap
"timeline" -> Timeline
"sankey-beta", "sankey" -> Sankey
"xychart-beta", "xychart" -> XyChart
"block-beta", "block" -> Block
"packet-beta", "packet" -> Packet
"kanban" -> Kanban
"architecture-beta", "architecture" -> Architecture
"radar-beta", "radar" -> Radar
"treemap-beta", "treemap" -> Treemap
else -> Unknown
}
}
Expand Down
Loading
Loading