Skip to content

KotlinLineNotes.kt

src/jvmMain/kotlin/afield/runtime/kotlin/KotlinLineNotes.kt · 59 lines

package afield.runtime.kotlin

import afield.document.graph.Box
import afield.ui.output.LastRunNotes

Line notes for a Kotlin box: each top-level val or var a run declared, and the value it held when the run finished.

The REPL hands back only a snippet's final value, but it also publishes the snippet's properties after every evaluation (box.ki.context.vars, the same map fromContext() searches), so a declaration's value can be read back by name once the run is over. A statement that declares nothing has no note: its value was never kept.

object KotlinLineNotes

Reads a finished run's declarations out of the box's variable table and records them as notes.

object KotlinLineNotes {

A top-level property declaration, modifiers and annotations allowed, at the start of a line.

    private val declaration = Regex("""^(?:(?:@[\w.]+(?:\([^)]*\))?|private|internal|public|lateinit|const)\s+)*(?:val|var)\s+([A-Za-z_]\w*)\b""")

fun record(box: Box, property: String, code: String, lineOffset: Int)

Record what the declarations of code (box property, after box line lineOffset) came to.

    fun record(box: Box, property: String, code: String, lineOffset: Int) {
        val vars = runCatching { box.ki.context.vars }.getOrNull() ?: return
        val produced = code.split("\n").mapIndexedNotNull { index, line ->
            val name = declaration.find(line)?.groupValues?.get(1) ?: return@mapIndexedNotNull null
            val info = vars[name] ?: return@mapIndexedNotNull null
            val shown = render(info.value)
            LastRunNotes.Produced(index + 1, index + 1, "$name = $shown",
                value = listOf(afield.ui.output.LineNotes.plain("$name = "), afield.ui.output.LineNotes.strong(shown)))
        }
        LastRunNotes.record(box, property, code, lineOffset, produced)
    }

internal fun render(value: Any?, depth: Int = 0): String

value on one short line. Collections, maps, arrays and long strings are cut before they are turned into text, so a note never builds the whole of a large value to show its start.

    internal fun render(value: Any?, depth: Int = 0): String {
        fun items(count: Int, first: Sequence<Any?>, open: String, close: String) =
            if (depth > 0) "$count items" else
                first.take(4).joinToString(", ", open, (if (count > 4) ", … ($count)" else "") + close) { render(it, depth + 1) }
        return runCatching {
            when (value) {
                null -> "null"
                is CharSequence -> "\"" + LastRunNotes.oneLine(value.subSequence(0, minOf(value.length, 80)).toString(), 80) + "\""
                is Collection<*> -> items(value.size, value.asSequence(), "[", "]")
                is Map<*, *> -> items(value.size, value.entries.asSequence().map { "${render(it.key, depth + 1)}=${render(it.value, depth + 1)}" }, "{", "}")
                is Array<*> -> items(value.size, value.asSequence(), "[", "]")
                is IntArray -> items(value.size, value.asSequence(), "[", "]")
                is FloatArray -> items(value.size, value.asSequence(), "[", "]")
                is DoubleArray -> items(value.size, value.asSequence(), "[", "]")
                is LongArray -> items(value.size, value.asSequence(), "[", "]")
                is ByteArray -> items(value.size, value.asSequence(), "[", "]")
                else -> LastRunNotes.oneLine(value.toString(), 80)
            }
        }.getOrElse { value?.javaClass?.simpleName ?: "null" }
    }
}