Skip to content

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('"')
    }