Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Each integration lives in the `integrations/` directory and has its own README.
- [IntelliJ Platform](integrations/intellij-platform/README.md) — interactive access to IntelliJ Platform APIs from a running IDE process.
- [HTTP Utilities](integrations/http-util/README.md) — JSON/serialization helpers and Ktor HTTP client wrappers for notebooks.
- [Database](integrations/database/README.md) — helpers for configuring JDBC DataSources and working with Databases.
- [Widgets](integrations/widgets/README.md) — ipywidgets API for Kotlin notebooks.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# https://intellij-support.jetbrains.com/hc/en-us/articles/206544879-Selecting-the-JDK-version-the-IDE-will-run-under
jvmTarget = "17"
kotlin = "2.2.20"
kotlin-jupyter = "0.15.1-668"
kotlin-jupyter = "0.15.1-761-1"
publishPlugin = "2.2.0-dev-71"
intellijPlatformGradlePlugin = "2.10.3"
intellijPlatform = "253-EAP-SNAPSHOT" # TODO: lower to 2025.1.3 GA whenever released with required changes
Expand Down
4 changes: 0 additions & 4 deletions integrations/intellij-platform/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ plugins {
val spaceUsername: String by properties
val spaceToken: String by properties

allprojects {
version = rootProject.version
}

kotlinJupyter {
addApiDependency()
}
Expand Down
35 changes: 35 additions & 0 deletions integrations/widgets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[![JetBrains official project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
[![Kotlin experimental stability](https://img.shields.io/badge/project-experimental-kotlin.svg?colorA=555555&colorB=AC29EC&label=&logo=kotlin&logoColor=ffffff&logoWidth=10)](https://kotlinlang.org/docs/components-stability.html)

# Kotlin Notebook Widgets Integration

This integration provides a collection of interactive widgets for Kotlin Notebooks, such as sliders, labels, and more. It allows you to create a richer, more interactive experience within your notebooks.

## Usage

Use this API through the `%use widgets` magic command in a Kotlin Notebook.

```kotlin
%use widgets

val slider = intSliderWidget().apply {
min = 0
max = 100
value = 50
description = "Select a value:"
}

val label = labelWidget().apply {
value = "Current value: ${slider.value}"
}

// Display the slider
slider
```

## Module structure

This project consists of the following modules:

- `widgets-api`: Contains the core widget implementations, protocols, and model definitions.
- `widgets-jupyter`: Provides the integration logic and useful helpers for Kotlin Jupyter notebooks.
24 changes: 24 additions & 0 deletions integrations/widgets/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import org.jetbrains.kotlinx.publisher.apache2
import org.jetbrains.kotlinx.publisher.githubRepo

plugins {
alias(libs.plugins.publisher)
}

kotlinPublications {
pom {
githubRepo("Kotlin", "kotlin-notebook-integrations")
inceptionYear = "2025"
licenses {
apache2()
}
developers {
developer {
id.set("kotlin-jupyter-team")
name.set("Kotlin Jupyter Team")
organization.set("JetBrains")
organizationUrl.set("https://www.jetbrains.com")
}
}
}
}
26 changes: 26 additions & 0 deletions integrations/widgets/widgets-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.publisher)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.jupyter.api)
}

dependencies {
compileOnly(libs.kotlinx.serialization.json)
implementation(libs.kotlin.reflect)
}

kotlin {
jvmToolchain(
libs.versions.jvm.toolchain
.get()
.toInt(),
)
explicitApi()
}

kotlinPublications {
publication {
description.set("Kotlin APIs for IPython Widgets")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.jetbrains.kotlinx.jupyter.widget

import org.jetbrains.kotlinx.jupyter.api.DisplayResult
import org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactoryRegistry
import org.jetbrains.kotlinx.jupyter.widget.model.WidgetModel

public interface WidgetManager {
public val factoryRegistry: WidgetFactoryRegistry

public fun getWidget(modelId: String): WidgetModel?

public fun getWidgetId(widget: WidgetModel): String?

public fun registerWidget(widget: WidgetModel)

public fun renderWidget(widget: WidgetModel): DisplayResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package org.jetbrains.kotlinx.jupyter.widget

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import org.jetbrains.kotlinx.jupyter.api.DisplayResult
import org.jetbrains.kotlinx.jupyter.api.MimeTypedResultEx
import org.jetbrains.kotlinx.jupyter.api.MimeTypes
import org.jetbrains.kotlinx.jupyter.api.libraries.Comm
import org.jetbrains.kotlinx.jupyter.api.libraries.CommManager
import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MAJOR_VERSION
import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MINOR_VERSION
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel
import org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactoryRegistry
import org.jetbrains.kotlinx.jupyter.widget.model.WidgetModel
import org.jetbrains.kotlinx.jupyter.widget.model.versionConstraintRegex
import org.jetbrains.kotlinx.jupyter.widget.protocol.CustomMessage
import org.jetbrains.kotlinx.jupyter.widget.protocol.RequestStateMessage
import org.jetbrains.kotlinx.jupyter.widget.protocol.RequestStatesMessage
import org.jetbrains.kotlinx.jupyter.widget.protocol.UpdateStatesMessage
import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetMessage
import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetOpenMessage
import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetStateMessage
import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetUpdateMessage
import org.jetbrains.kotlinx.jupyter.widget.protocol.getWireMessage
import org.jetbrains.kotlinx.jupyter.widget.protocol.toPatch

private val widgetOpenMetadataJson =
buildJsonObject {
put("version", "${DEFAULT_MAJOR_VERSION}.${DEFAULT_MINOR_VERSION}")
}

public class WidgetManagerImpl(
private val commManager: CommManager,
private val classLoaderProvider: () -> ClassLoader,
) : WidgetManager {
private val widgetTarget = "jupyter.widget"
private val widgetControlTarget = "jupyter.widget.control"
private val widgets = mutableMapOf<String, WidgetModel>()
private val widgetIdByWidget = mutableMapOf<WidgetModel, String>()

override val factoryRegistry: WidgetFactoryRegistry = WidgetFactoryRegistry()

init {
commManager.registerCommTarget(widgetControlTarget) { comm, _, _, _ ->
comm.onMessage { msg, _, _ ->
when (Json.decodeFromJsonElement<WidgetMessage>(msg)) {
is RequestStatesMessage -> {
val fullStates =
widgets.mapValues { (id, widget) ->
widget.getFullState()
}

val wireMessage = getWireMessage(fullStates)
val message = UpdateStatesMessage(wireMessage.state, wireMessage.bufferPaths)

val data = Json.encodeToJsonElement<WidgetMessage>(message).jsonObject
comm.send(data, null, emptyList())
}

else -> {}
}
}
}

commManager.registerCommTarget(widgetTarget) { comm, data, _, buffers ->
val openMessage = Json.decodeFromJsonElement<WidgetOpenMessage>(data)
val modelName = openMessage.state["_model_name"]?.jsonPrimitive?.content!!
val widgetFactory = factoryRegistry.loadWidgetFactory(modelName, classLoaderProvider())

val widget = widgetFactory.create(this)
val patch = openMessage.toPatch(buffers)
widget.applyPatch(patch)

initializeWidget(comm, widget)
}
}

override fun getWidget(modelId: String): WidgetModel? = widgets[modelId]

override fun getWidgetId(widget: WidgetModel): String? = widgetIdByWidget[widget]

override fun registerWidget(widget: WidgetModel) {
if (getWidgetId(widget) != null) return

val fullState = widget.getFullState()
val wireMessage = getWireMessage(fullState)

val comm =
commManager.openComm(
widgetTarget,
Json
.encodeToJsonElement(
WidgetOpenMessage(
wireMessage.state,
wireMessage.bufferPaths,
),
).jsonObject,
widgetOpenMetadataJson,
wireMessage.buffers,
)

initializeWidget(comm, widget)
}

override fun renderWidget(widget: WidgetModel): DisplayResult =
MimeTypedResultEx(
buildJsonObject {
val modelId = getWidgetId(widget) ?: error("Widget is not registered")
var versionMajor = DEFAULT_MAJOR_VERSION
var versionMinor = DEFAULT_MINOR_VERSION
var modelName: String? = null
if (widget is DefaultWidgetModel) {
modelName = widget.modelName
val version = widget.modelModuleVersion
val matchResult = versionConstraintRegex.find(version)
if (matchResult != null) {
versionMajor = matchResult.groupValues[1].toInt()
versionMinor = matchResult.groupValues[2].toInt()
}
}
if (modelName != null) {
put(MimeTypes.HTML, "$modelName(id=$modelId)")
}
put(
"application/vnd.jupyter.widget-view+json",
buildJsonObject {
put("version_major", versionMajor)
put("version_minor", versionMinor)
put("model_id", modelId)
},
)
},
null,
)

private fun initializeWidget(
comm: Comm,
widget: WidgetModel,
) {
val modelId = comm.id
widgetIdByWidget[widget] = modelId
widgets[modelId] = widget

// Reflect kernel-side changes on the frontend
widget.addChangeListener { patch ->
val wireMessage = getWireMessage(patch)
val data =
Json
.encodeToJsonElement<WidgetMessage>(
WidgetUpdateMessage(
wireMessage.state,
wireMessage.bufferPaths,
),
).jsonObject
comm.send(data, null, wireMessage.buffers)
}

// Reflect frontend-side changes on kernel
comm.onMessage { msg, _, buffers ->
when (val message = Json.decodeFromJsonElement<WidgetMessage>(msg)) {
is WidgetStateMessage -> {
widget.applyPatch(message.toPatch(buffers))
}

is RequestStateMessage -> {
val fullState = widget.getFullState()
val wireMessage = getWireMessage(fullState)
val data =
Json
.encodeToJsonElement<WidgetMessage>(
WidgetUpdateMessage(
wireMessage.state,
wireMessage.bufferPaths,
),
).jsonObject
comm.send(data, null, wireMessage.buffers)
}

is CustomMessage -> {}

else -> {}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.jetbrains.kotlinx.jupyter.widget.library

import org.jetbrains.kotlinx.jupyter.widget.WidgetManager
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel
import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec
import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget

public fun WidgetManager.html(): HtmlWidget = createAndRegisterWidget(HtmlWidget.Factory)

private val spec = controlsSpec("HTML")

public class HtmlWidget internal constructor(
widgetManager: WidgetManager,
) : DefaultWidgetModel(spec, widgetManager) {
internal object Factory : DefaultWidgetFactory<HtmlWidget>(spec, ::HtmlWidget)

public var value: String by stringProp("value")
public var layout: LayoutWidget? by widgetProp("layout")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.jetbrains.kotlinx.jupyter.widget.library

import org.jetbrains.kotlinx.jupyter.widget.WidgetManager
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel
import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec
import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget

public fun WidgetManager.intSlider(): IntSliderWidget = createAndRegisterWidget(IntSliderWidget.Factory)

private val spec = controlsSpec("IntSlider")

public class IntSliderWidget internal constructor(
widgetManager: WidgetManager,
) : DefaultWidgetModel(spec, widgetManager) {
internal object Factory : DefaultWidgetFactory<IntSliderWidget>(spec, ::IntSliderWidget)

public var value: Int by intProp("value", 0)
public var min: Int by intProp("min", 0)
public var max: Int by intProp("max", 100)
public var step: Int by intProp("step", 1)
public var description: String by stringProp("description", "")
public var layout: LayoutWidget? by widgetProp("layout")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.jetbrains.kotlinx.jupyter.widget.library

import org.jetbrains.kotlinx.jupyter.widget.WidgetManager
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory
import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel
import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec
import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget

public fun WidgetManager.label(): LabelWidget = createAndRegisterWidget(LabelWidget.Factory)

private val spec = controlsSpec("Label")

public class LabelWidget internal constructor(
widgetManager: WidgetManager,
) : DefaultWidgetModel(spec, widgetManager) {
internal object Factory : DefaultWidgetFactory<LabelWidget>(spec, ::LabelWidget)

public var value: String by stringProp("value", "")
}
Loading