Skip to content

Commit c900ce2

Browse files
committed
KTNB-1205: Support Jupyter Widgets - platform and 5 kinds of widgets
1 parent 809c124 commit c900ce2

38 files changed

+1278
-5
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# https://intellij-support.jetbrains.com/hc/en-us/articles/206544879-Selecting-the-JDK-version-the-IDE-will-run-under
44
jvmTarget = "17"
55
kotlin = "2.2.20"
6-
kotlin-jupyter = "0.15.1-668"
6+
kotlin-jupyter = "0.15.1-761-1"
77
publishPlugin = "2.2.0-dev-71"
88
intellijPlatformGradlePlugin = "2.10.3"
99
intellijPlatform = "253-EAP-SNAPSHOT" # TODO: lower to 2025.1.3 GA whenever released with required changes

integrations/intellij-platform/build.gradle.kts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ plugins {
1212
val spaceUsername: String by properties
1313
val spaceToken: String by properties
1414

15-
allprojects {
16-
version = rootProject.version
17-
}
18-
1915
kotlinJupyter {
2016
addApiDependency()
2117
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import org.jetbrains.kotlinx.publisher.apache2
2+
import org.jetbrains.kotlinx.publisher.githubRepo
3+
4+
plugins {
5+
alias(libs.plugins.publisher)
6+
}
7+
8+
kotlinPublications {
9+
pom {
10+
githubRepo("Kotlin", "kotlin-notebook-integrations")
11+
inceptionYear = "2025"
12+
licenses {
13+
apache2()
14+
}
15+
developers {
16+
developer {
17+
id.set("kotlin-jupyter-team")
18+
name.set("Kotlin Jupyter Team")
19+
organization.set("JetBrains")
20+
organizationUrl.set("https://www.jetbrains.com")
21+
}
22+
}
23+
}
24+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
plugins {
2+
alias(libs.plugins.kotlin.jvm)
3+
alias(libs.plugins.publisher)
4+
alias(libs.plugins.kotlin.serialization)
5+
alias(libs.plugins.kotlin.jupyter.api)
6+
}
7+
8+
dependencies {
9+
compileOnly(libs.kotlinx.serialization.json)
10+
11+
testImplementation(libs.kotlin.test)
12+
testImplementation(libs.kotlinx.serialization.json)
13+
}
14+
15+
kotlin {
16+
jvmToolchain(
17+
libs.versions.jvm.toolchain
18+
.get()
19+
.toInt(),
20+
)
21+
explicitApi()
22+
}
23+
24+
tasks.processJupyterApiResources {
25+
libraryProducers = listOf("org.jetbrains.kotlinx.jupyter.widget.WidgetJupyterIntegration")
26+
}
27+
28+
kotlinPublications {
29+
publication {
30+
description.set("Kotlin APIs for IPython Widgets")
31+
}
32+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.jetbrains.kotlinx.jupyter.widget
2+
3+
import org.jetbrains.kotlinx.jupyter.api.declare
4+
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
5+
import org.jetbrains.kotlinx.jupyter.widget.library.IntSliderWidget
6+
import org.jetbrains.kotlinx.jupyter.widget.model.WidgetModel
7+
8+
private var myWidgetManager: WidgetManager? = null
9+
internal val globalWidgetManager: WidgetManager get() = myWidgetManager!!
10+
11+
public class WidgetJupyterIntegration : JupyterIntegration() {
12+
override fun Builder.onLoaded() {
13+
importPackage<WidgetJupyterIntegration>()
14+
importPackage<IntSliderWidget>()
15+
16+
var myLastClassLoader = WidgetJupyterIntegration::class.java.classLoader
17+
val widgetManager = WidgetManager(notebook.commManager) { myLastClassLoader }
18+
myWidgetManager = widgetManager
19+
20+
onLoaded {
21+
myLastClassLoader = this.lastClassLoader
22+
23+
declare("widgetManager" to widgetManager)
24+
}
25+
26+
afterCellExecution { _, _ ->
27+
myLastClassLoader = this.lastClassLoader
28+
}
29+
30+
renderWithHost<WidgetModel> { _, widget ->
31+
widgetManager.renderWidget(widget)
32+
}
33+
}
34+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package org.jetbrains.kotlinx.jupyter.widget
2+
3+
import kotlinx.serialization.json.Json
4+
import kotlinx.serialization.json.buildJsonObject
5+
import kotlinx.serialization.json.decodeFromJsonElement
6+
import kotlinx.serialization.json.encodeToJsonElement
7+
import kotlinx.serialization.json.jsonObject
8+
import kotlinx.serialization.json.jsonPrimitive
9+
import kotlinx.serialization.json.put
10+
import org.jetbrains.kotlinx.jupyter.api.DisplayResult
11+
import org.jetbrains.kotlinx.jupyter.api.MimeTypedResultEx
12+
import org.jetbrains.kotlinx.jupyter.api.MimeTypes
13+
import org.jetbrains.kotlinx.jupyter.api.libraries.Comm
14+
import org.jetbrains.kotlinx.jupyter.api.libraries.CommManager
15+
import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MAJOR_VERSION
16+
import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MINOR_VERSION
17+
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel
18+
import org.jetbrains.kotlinx.jupyter.widget.model.WidgetModel
19+
import org.jetbrains.kotlinx.jupyter.widget.model.loadWidgetFactory
20+
import org.jetbrains.kotlinx.jupyter.widget.model.versionConstraintRegex
21+
import org.jetbrains.kotlinx.jupyter.widget.protocol.CustomMessage
22+
import org.jetbrains.kotlinx.jupyter.widget.protocol.RequestStateMessage
23+
import org.jetbrains.kotlinx.jupyter.widget.protocol.RequestStatesMessage
24+
import org.jetbrains.kotlinx.jupyter.widget.protocol.UpdateStatesMessage
25+
import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetMessage
26+
import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetOpenMessage
27+
import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetStateMessage
28+
import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetUpdateMessage
29+
import org.jetbrains.kotlinx.jupyter.widget.protocol.getWireMessage
30+
import org.jetbrains.kotlinx.jupyter.widget.protocol.toPatch
31+
32+
private val widgetOpenMetadataJson =
33+
buildJsonObject {
34+
put("version", "${DEFAULT_MAJOR_VERSION}.${DEFAULT_MINOR_VERSION}")
35+
}
36+
37+
public class WidgetManager(
38+
private val commManager: CommManager,
39+
private val classLoaderProvider: () -> ClassLoader,
40+
) {
41+
private val widgetTarget = "jupyter.widget"
42+
private val widgetControlTarget = "jupyter.widget.control"
43+
private val widgets = mutableMapOf<String, WidgetModel>()
44+
45+
init {
46+
commManager.registerCommTarget(widgetControlTarget) { comm, _, _, _ ->
47+
comm.onMessage { msg, _, _ ->
48+
when (Json.decodeFromJsonElement<WidgetMessage>(msg)) {
49+
is RequestStatesMessage -> {
50+
val fullStates =
51+
widgets.mapValues { (id, widget) ->
52+
widget.getFullState()
53+
}
54+
55+
val wireMessage = getWireMessage(fullStates)
56+
val message = UpdateStatesMessage(wireMessage.state, wireMessage.bufferPaths)
57+
58+
val data = Json.encodeToJsonElement<WidgetMessage>(message).jsonObject
59+
comm.send(data, null, emptyList())
60+
}
61+
62+
else -> {}
63+
}
64+
}
65+
}
66+
67+
commManager.registerCommTarget(widgetTarget) { comm, data, _, buffers ->
68+
val openMessage = Json.decodeFromJsonElement<WidgetOpenMessage>(data)
69+
val modelName = openMessage.state["_model_name"]?.jsonPrimitive?.content!!
70+
val widgetFactory = loadWidgetFactory(modelName, classLoaderProvider())
71+
72+
val widget = widgetFactory.create()
73+
val patch = openMessage.toPatch(buffers)
74+
widget.applyPatch(patch, this)
75+
76+
initializeWidget(comm, widget)
77+
}
78+
}
79+
80+
public fun getWidget(modelId: String): WidgetModel? = widgets[modelId]
81+
82+
public fun registerWidget(widget: WidgetModel) {
83+
if (widget.id != null) return
84+
85+
val fullState = widget.getFullState()
86+
val wireMessage = getWireMessage(fullState)
87+
88+
val comm =
89+
commManager.openComm(
90+
widgetTarget,
91+
Json
92+
.encodeToJsonElement(
93+
WidgetOpenMessage(
94+
wireMessage.state,
95+
wireMessage.bufferPaths,
96+
),
97+
).jsonObject,
98+
widgetOpenMetadataJson,
99+
wireMessage.buffers,
100+
)
101+
102+
initializeWidget(comm, widget)
103+
}
104+
105+
public fun renderWidget(widget: WidgetModel): DisplayResult =
106+
MimeTypedResultEx(
107+
buildJsonObject {
108+
val modelId = widget.id ?: error("Widget is not registered")
109+
var versionMajor = DEFAULT_MAJOR_VERSION
110+
var versionMinor = DEFAULT_MINOR_VERSION
111+
var modelName: String? = null
112+
if (widget is DefaultWidgetModel) {
113+
modelName = widget.modelName
114+
val version = widget.modelModuleVersion
115+
val matchResult = versionConstraintRegex.find(version)
116+
if (matchResult != null) {
117+
versionMajor = matchResult.groupValues[1].toInt()
118+
versionMinor = matchResult.groupValues[2].toInt()
119+
}
120+
}
121+
if (modelName != null) {
122+
put(MimeTypes.HTML, "$modelName(id=$modelId)")
123+
}
124+
put(
125+
"application/vnd.jupyter.widget-view+json",
126+
buildJsonObject {
127+
put("version_major", versionMajor)
128+
put("version_minor", versionMinor)
129+
put("model_id", modelId)
130+
},
131+
)
132+
},
133+
null,
134+
)
135+
136+
private fun initializeWidget(
137+
comm: Comm,
138+
widget: WidgetModel,
139+
) {
140+
val modelId = comm.id
141+
widget.setModelId(modelId)
142+
widgets[modelId] = widget
143+
144+
// Reflect kernel-side changes on the frontend
145+
widget.addChangeListener { patch ->
146+
val wireMessage = getWireMessage(patch)
147+
val data =
148+
Json
149+
.encodeToJsonElement<WidgetMessage>(
150+
WidgetUpdateMessage(
151+
wireMessage.state,
152+
wireMessage.bufferPaths,
153+
),
154+
).jsonObject
155+
comm.send(data, null, wireMessage.buffers)
156+
}
157+
158+
// Reflect frontend-side changes on kernel
159+
comm.onMessage { msg, _, buffers ->
160+
when (val message = Json.decodeFromJsonElement<WidgetMessage>(msg)) {
161+
is WidgetStateMessage -> {
162+
widget.applyPatch(message.toPatch(buffers), this)
163+
}
164+
165+
is RequestStateMessage -> {
166+
val fullState = widget.getFullState()
167+
val wireMessage = getWireMessage(fullState)
168+
val data =
169+
Json
170+
.encodeToJsonElement<WidgetMessage>(
171+
WidgetUpdateMessage(
172+
wireMessage.state,
173+
wireMessage.bufferPaths,
174+
),
175+
).jsonObject
176+
comm.send(data, null, wireMessage.buffers)
177+
}
178+
179+
is CustomMessage -> {}
180+
181+
else -> {}
182+
}
183+
}
184+
}
185+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.jetbrains.kotlinx.jupyter.widget.library
2+
3+
import org.jetbrains.kotlinx.jupyter.widget.WidgetManager
4+
import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager
5+
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory
6+
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel
7+
import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec
8+
import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec
9+
import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget
10+
11+
public fun WidgetManager.html(): HtmlWidget = createAndRegisterWidget(HtmlWidget.Factory::class)
12+
13+
public fun htmlWidget(): HtmlWidget = globalWidgetManager.html()
14+
15+
public class HtmlWidget internal constructor(
16+
spec: WidgetSpec,
17+
) : DefaultWidgetModel(spec) {
18+
internal class Factory :
19+
DefaultWidgetFactory<HtmlWidget>(
20+
controlsSpec("HTML"),
21+
::HtmlWidget,
22+
)
23+
24+
public var value: String by stringProp("value")
25+
public var layout: LayoutWidget? by widgetProp("layout")
26+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.jetbrains.kotlinx.jupyter.widget.library
2+
3+
import org.jetbrains.kotlinx.jupyter.widget.WidgetManager
4+
import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager
5+
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory
6+
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel
7+
import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec
8+
import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec
9+
import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget
10+
11+
public fun WidgetManager.intSlider(): IntSliderWidget = createAndRegisterWidget(IntSliderWidget.Factory::class)
12+
13+
public fun intSliderWidget(): IntSliderWidget = globalWidgetManager.intSlider()
14+
15+
public class IntSliderWidget internal constructor(
16+
spec: WidgetSpec,
17+
) : DefaultWidgetModel(spec) {
18+
internal class Factory :
19+
DefaultWidgetFactory<IntSliderWidget>(
20+
controlsSpec("IntSlider"),
21+
::IntSliderWidget,
22+
)
23+
24+
public var value: Int by intProp("value", 0)
25+
public var min: Int by intProp("min", 0)
26+
public var max: Int by intProp("max", 100)
27+
public var step: Int by intProp("step", 1)
28+
public var description: String by stringProp("description", "")
29+
public var layout: LayoutWidget? by widgetProp("layout")
30+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.jetbrains.kotlinx.jupyter.widget.library
2+
3+
import org.jetbrains.kotlinx.jupyter.widget.WidgetManager
4+
import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager
5+
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory
6+
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel
7+
import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec
8+
import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec
9+
import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget
10+
11+
public fun WidgetManager.label(): LabelWidget = createAndRegisterWidget(LabelWidget.Factory::class)
12+
13+
public fun labelWidget(): LabelWidget = globalWidgetManager.label()
14+
15+
public class LabelWidget internal constructor(
16+
spec: WidgetSpec,
17+
) : DefaultWidgetModel(spec) {
18+
internal class Factory :
19+
DefaultWidgetFactory<LabelWidget>(
20+
controlsSpec("Label"),
21+
::LabelWidget,
22+
)
23+
24+
public var value: String by stringProp("value", "")
25+
}

0 commit comments

Comments
 (0)