tryling: better error reporting

This commit is contained in:
Sergey Chernov 2025-11-21 09:44:48 +01:00
parent 01632dc6d7
commit a229f227e1

View File

@ -19,6 +19,7 @@ import androidx.compose.runtime.*
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScriptError
import net.sergeych.site.SiteHighlight import net.sergeych.site.SiteHighlight
import org.jetbrains.compose.web.attributes.placeholder import org.jetbrains.compose.web.attributes.placeholder
import org.jetbrains.compose.web.dom.* import org.jetbrains.compose.web.dom.*
@ -44,6 +45,7 @@ fun TryLyngPage() {
var running by remember { mutableStateOf(false) } var running by remember { mutableStateOf(false) }
var output by remember { mutableStateOf<String?>(null) } var output by remember { mutableStateOf<String?>(null) }
var error by remember { mutableStateOf<String?>(null) } var error by remember { mutableStateOf<String?>(null) }
var extendedError by remember { mutableStateOf<String?>(null) }
fun runCode() { fun runCode() {
if (running) return if (running) return
@ -91,18 +93,27 @@ fun TryLyngPage() {
// Prefer detailed message including stack if available (K/JS) // Prefer detailed message including stack if available (K/JS)
val errText = buildString { val errText = buildString {
append(t.toString()) append(t.toString())
try { if (t !is ScriptError) {
val st = t.asDynamic().stack as? String try {
if (!st.isNullOrBlank()) { val st = t.asDynamic().stack as? String
append("\n") if (!st.isNullOrBlank()) {
append(st) append("\n")
append(st)
}
} catch (_: Throwable) {
} }
} catch (_: Throwable) {} }
} }
if (printed.isNotEmpty()) { if (printed.isNotEmpty()) {
output = printed.toString() output = printed.toString()
} }
error = errText if (t is ScriptError) {
error = t.errorMessage
extendedError = errText
} else {
error = t.message
extendedError = errText
}
} finally { } finally {
running = false running = false
} }
@ -182,10 +193,10 @@ fun TryLyngPage() {
if (output != null) { if (output != null) {
Pre({ classes("mb-0") }) { Code { Text(output!!) } } Pre({ classes("mb-0") }) { Code { Text(output!!) } }
} }
if (error != null) { if (extendedError != null) {
if (output != null) Hr({}) if (output != null) Hr({})
Div({ classes("alert", "alert-danger", "mb-0") }) { Div({ classes("alert", "alert-danger", "mb-0") }) {
Pre({ classes("mb-0") }) { Code { Text(error!!) } } Pre({ classes("mb-0") }) { Code { Text(extendedError!!) } }
} }
} }
} }
@ -275,8 +286,8 @@ private fun EditorWithOverlay(
} }
fun appendSentinel(html: String): String = fun appendSentinel(html: String): String =
// Append a zero-width space sentinel to keep the last line box from collapsing // Append a zero-width space sentinel to keep the last line box from collapsing
// in some browsers, which can otherwise cause caret size/position glitches // in some browsers, which can otherwise cause caret size/position glitches
// when the caret is at end-of-line. // when the caret is at end-of-line.
html + "<span data-sentinel=\"1\">&#8203;</span>" html + "<span data-sentinel=\"1\">&#8203;</span>"
@ -357,6 +368,7 @@ private fun EditorWithOverlay(
val ta = taEl ?: return@LaunchedEffect val ta = taEl ?: return@LaunchedEffect
val ov = overlayEl ?: return@LaunchedEffect val ov = overlayEl ?: return@LaunchedEffect
val cs = window.getComputedStyle(ta) val cs = window.getComputedStyle(ta)
// Resolve a concrete pixel line-height; some browsers return "normal" or unitless // Resolve a concrete pixel line-height; some browsers return "normal" or unitless
fun ensurePxLineHeight(): String { fun ensurePxLineHeight(): String {
val lh = cs.lineHeight ?: "" val lh = cs.lineHeight ?: ""
@ -368,10 +380,14 @@ private fun EditorWithOverlay(
probe.textContent = "M" probe.textContent = "M"
val fw = try { val fw = try {
(cs.asDynamic().fontWeight as? String) ?: cs.getPropertyValue("font-weight") (cs.asDynamic().fontWeight as? String) ?: cs.getPropertyValue("font-weight")
} catch (_: Throwable) { null } } catch (_: Throwable) {
null
}
val fs = try { val fs = try {
(cs.asDynamic().fontStyle as? String) ?: cs.getPropertyValue("font-style") (cs.asDynamic().fontStyle as? String) ?: cs.getPropertyValue("font-style")
} catch (_: Throwable) { null } } catch (_: Throwable) {
null
}
probe.setAttribute( probe.setAttribute(
"style", "style",
buildString { buildString {
@ -393,6 +409,7 @@ private fun EditorWithOverlay(
val approx = if (fsPx != null) fsPx * 1.2 else 16.0 * 1.2 val approx = if (fsPx != null) fsPx * 1.2 else 16.0 * 1.2
return "${'$'}{approx}px" return "${'$'}{approx}px"
} }
val lineHeightPx = ensurePxLineHeight() val lineHeightPx = ensurePxLineHeight()
// copy key properties // copy key properties
val style = buildString { val style = buildString {
@ -408,11 +425,15 @@ private fun EditorWithOverlay(
// Try to mirror weight and style to eliminate metric differences // Try to mirror weight and style to eliminate metric differences
val fw = try { val fw = try {
(cs.asDynamic().fontWeight as? String) ?: cs.getPropertyValue("font-weight") (cs.asDynamic().fontWeight as? String) ?: cs.getPropertyValue("font-weight")
} catch (_: Throwable) { null } } catch (_: Throwable) {
null
}
if (!fw.isNullOrBlank()) append("font-weight:").append(fw).append(";") if (!fw.isNullOrBlank()) append("font-weight:").append(fw).append(";")
val fs = try { val fs = try {
(cs.asDynamic().fontStyle as? String) ?: cs.getPropertyValue("font-style") (cs.asDynamic().fontStyle as? String) ?: cs.getPropertyValue("font-style")
} catch (_: Throwable) { null } } catch (_: Throwable) {
null
}
if (!fs.isNullOrBlank()) append("font-style:").append(fs).append(";") if (!fs.isNullOrBlank()) append("font-style:").append(fs).append(";")
// Disable ligatures in overlay to keep glyph advances identical to textarea // Disable ligatures in overlay to keep glyph advances identical to textarea
append("font-variant-ligatures:none;") append("font-variant-ligatures:none;")
@ -433,8 +454,10 @@ private fun EditorWithOverlay(
if (!existing.contains("line-height")) { if (!existing.contains("line-height")) {
ta.setAttribute("style", existing + " line-height: " + lineHeightPx + ";") ta.setAttribute("style", existing + " line-height: " + lineHeightPx + ";")
} }
} catch (_: Throwable) {} } catch (_: Throwable) {
} catch (_: Throwable) {} }
} catch (_: Throwable) {
}
} }
// container // container
@ -558,7 +581,8 @@ private fun EditorWithOverlay(
if (start != end) { if (start != end) {
// Indent selected lines // Indent selected lines
val regionStart = currentLineStartIndex(code, start) val regionStart = currentLineStartIndex(code, start)
val regionEnd = code.indexOf('\n', startIndex = end).let { if (it == -1) code.length else it } val regionEnd =
code.indexOf('\n', startIndex = end).let { if (it == -1) code.length else it }
val region = code.substring(regionStart, regionEnd) val region = code.substring(regionStart, regionEnd)
val lines = region.split("\n") val lines = region.split("\n")
val indentStr = " ".repeat(tabSize) val indentStr = " ".repeat(tabSize)
@ -591,6 +615,7 @@ private fun EditorWithOverlay(
} }
} }
} }
"Enter" -> { "Enter" -> {
ev.preventDefault() ev.preventDefault()
val before = code.substring(0, start) val before = code.substring(0, start)
@ -610,6 +635,7 @@ private fun EditorWithOverlay(
pendingScrollLeft = savedScrollLeft pendingScrollLeft = savedScrollLeft
setCode(newCode) setCode(newCode)
} }
"}" -> { "}" -> {
// If the current line contains only indentation up to the caret, // If the current line contains only indentation up to the caret,
// outdent by one indent level (tab or up to tabSize spaces) before inserting '}'. // outdent by one indent level (tab or up to tabSize spaces) before inserting '}'.