LanguageRuntimeSupport.kt
src/jvmMain/kotlin/afield/runtime/LanguageRuntimeSupport.kt · 152 lines
package afield.runtime
import afield.document.graph.Box
import afield.runtime.execution.BoxExecution
import afield.runtime.execution.KotlinExecution
import afield.lang.remotejs.code_language
import afield.app.diagnostics.logging.lblock
import afield.runtime.lifecycle.inMain
import afield.app.notifications.notify
import afield.document.graph.offline
import afield.app.server.sessions
import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly
import java.util.WeakHashMap
import kotlin.reflect.KClass
Shared machinery for a language runtime that converts a language root's children to its own execution.
TopologyWatcher runs a callback on the runloop whenever a session's box graph changes.
applyRuntimeToggle does the conversion: each child of a language root gets the runtime's
execution, a language label in its corner and the matching code_language. A child converts back
to Kotlin only when it has no language-root parent left, so the order of connecting and
disconnecting does not matter.
class TopologyWatcher
Polls every session's topology generation on the runloop and calls back with its boxes when it changes.
class TopologyWatcher(
private val label: String,
private val intervalMs: Long = 5L,
private val onTopologyChanged: (Collection<Box>) -> Unit
) {
private val lastGeneration = WeakHashMap<Any, Int>()
fun start()
fun start() = inMain {
while (true) {
sessions.entries.forEach { entry ->
val current = entry.value.topologyGeneration
val prev = lastGeneration.put(entry.key, current)
if (prev == null || prev != current) {
onTopologyChanged(entry.value.boxes.values)
}
}
if (intervalMs <= 0) {
yield()
} else {
delay(intervalMs)
}
}
}
}
fun applyRuntimeToggle
body — 97 lines, from line 55
fun applyRuntimeToggle(
boxes: Collection<Box>,
isRoot: (Box) -> Boolean?,
runtimeClass: KClass<out BoxExecution>,
newExecution: (Box) -> BoxExecution,
language: String,
customization: String,
notifyCategory: String,
onAttach: (Box) -> Unit = {},
onDetach: (Box) -> Unit = {}
) {
boxes.forEach { root ->
val targetLanguage = language.capitalizeAsciiOnly()
if (isRoot(root) == true) {
root.children.forEach { child ->
if (runtimeClass.isInstance(child.ke)) {
child.offline = false
onAttach(child)
return@forEach
}
child.ke = newExecution(child)
child.customization = languageLabelCustomization(language)
child.code_language = language
child.offline = false
onAttach(child)
notify("'${child.name}' becomes $targetLanguage", notifyCategory)
}
} else {
root.children.forEach { child ->
if (runtimeClass.isInstance(child.ke)) {
// A box stays in this language while it still has a language-root
// parent (e.g. a notebook cell that is a child of BOTH the document
// root and a python-root). Only revert boxes with no language-root
// parent left — this makes the toggle order-independent and means the
// language-root parent, not exclusive parenting, is what confers the
// language.
if (child.parents.any { isRoot(it) == true }) return@forEach
onDetach(child)
child.ke = KotlinExecution()
child.customization = languageLabelCustomization(null)
child.code_language = "Kotlin"
notify("'${child.name}' no longer $targetLanguage", notifyCategory)
}
}
}
}
}
private fun languageLabelCustomization(language: String?): String {
val trimmed = language?.trim().orEmpty()
if (trimmed.isBlank() || trimmed.equals("kotlin", ignoreCase = true)) {
return """
|const existing = this.querySelector('[data-afield-language-label="true"]');
|if (existing) existing.remove();
|this.style.margin = "";
""".trimMargin()
}
return """
|const title = this.querySelector('.box-title-stack') || this;
|let label = this.querySelector('[data-afield-language-label="true"]');
|if (!label) {
| label = document.createElement('div');
| label.setAttribute('data-afield-language-label', 'true');
| title.appendChild(label);
|}
|label.textContent = ${jsString(trimmed)};
|label.style.fontSize = "9px";
|label.style.lineHeight = "1.1";
|label.style.opacity = "0.58";
|label.style.fontWeight = "500";
|label.style.letterSpacing = "0";
|label.style.textTransform = "none";
|label.style.pointerEvents = "none";
|label.style.userSelect = "none";
|label.style.marginTop = "1px";
|this.style.margin = "";
""".trimMargin()
}
private fun jsString(value: String): String =
buildString {
append('"')
value.forEach { c ->
append(
when (c) {
'\\' -> "\\\\"
'"' -> "\\\""
'\n' -> "\\n"
'\r' -> "\\r"
'\t' -> "\\t"
else -> c
},
)
}
append('"')
}