language tools and site suport
This commit is contained in:
1 parent
78d8e546d5
commit
c16c0d7ebd
16 files changed
+1402
-338
No files matched your search
@@ -120,6 +120,12 @@ fun ReferencePage() {
|
||||
Div { Text("$kind ${d.name}$t") }
|
||||
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
|
||||
}
|
||||
is MiniTypeAliasDecl -> {
|
||||
val params = if (d.typeParams.isEmpty()) "" else d.typeParams.joinToString(", ", "<", ">")
|
||||
val target = DocLookupUtils.typeOf(d.target).ifEmpty { "Any" }
|
||||
Div { Text("type ${d.name}$params = $target") }
|
||||
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
|
||||
}
|
||||
is MiniClassDecl -> {
|
||||
Div { Text("class ${d.name}") }
|
||||
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
|
||||
@@ -145,6 +151,12 @@ fun ReferencePage() {
|
||||
val staticStr = if (m.isStatic) "static " else ""
|
||||
Li { Text("${staticStr}${kindM} ${d.name}.${m.name}${ts}") }
|
||||
}
|
||||
is MiniMemberTypeAliasDecl -> {
|
||||
val params = if (m.typeParams.isEmpty()) "" else m.typeParams.joinToString(", ", "<", ">")
|
||||
val target = DocLookupUtils.typeOf(m.target).ifEmpty { "Any" }
|
||||
val staticStr = if (m.isStatic) "static " else ""
|
||||
Li { Text("${staticStr}type ${d.name}.${m.name}$params = $target") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,15 @@ import kotlinx.coroutines.launch
|
||||
import net.sergeych.lyng.LyngVersion
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.ScriptError
|
||||
import net.sergeych.lyng.highlight.TextRange
|
||||
import net.sergeych.lyng.miniast.CompletionItem
|
||||
import net.sergeych.lyng.tools.LyngDiagnostic
|
||||
import net.sergeych.lyng.tools.LyngDiagnosticSeverity
|
||||
import net.sergeych.lyng.tools.LyngSymbolInfo
|
||||
import net.sergeych.lyng.tools.LyngSymbolTarget
|
||||
import net.sergeych.lyngweb.EditorWithOverlay
|
||||
import net.sergeych.lyngweb.LyngWebTools
|
||||
import org.jetbrains.compose.web.attributes.InputType
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
|
||||
@Composable
|
||||
@@ -52,6 +60,15 @@ fun TryLyngPage(route: String) {
|
||||
var output by remember { mutableStateOf<String?>(null) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
var extendedError by remember { mutableStateOf<String?>(null) }
|
||||
var diagnostics by remember { mutableStateOf<List<LyngDiagnostic>>(emptyList()) }
|
||||
var completionItems by remember { mutableStateOf<List<CompletionItem>>(emptyList()) }
|
||||
var completionOffset by remember { mutableStateOf<Int?>(null) }
|
||||
var docInfo by remember { mutableStateOf<LyngSymbolInfo?>(null) }
|
||||
var definitionTarget by remember { mutableStateOf<LyngSymbolTarget?>(null) }
|
||||
var usageRanges by remember { mutableStateOf<List<TextRange>>(emptyList()) }
|
||||
var disasmSymbol by remember { mutableStateOf<String>("") }
|
||||
var disasmOutput by remember { mutableStateOf<String?>(null) }
|
||||
var disasmError by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
fun runCode() {
|
||||
if (running) return
|
||||
@@ -59,6 +76,14 @@ fun TryLyngPage(route: String) {
|
||||
output = null
|
||||
error = null
|
||||
extendedError = null
|
||||
completionItems = emptyList()
|
||||
completionOffset = null
|
||||
docInfo = null
|
||||
definitionTarget = null
|
||||
usageRanges = emptyList()
|
||||
diagnostics = emptyList()
|
||||
disasmOutput = null
|
||||
disasmError = null
|
||||
scope.launch {
|
||||
// keep this outside try so we can show partial prints if evaluation fails
|
||||
val printed = StringBuilder()
|
||||
@@ -156,6 +181,22 @@ fun TryLyngPage(route: String) {
|
||||
runCode()
|
||||
}
|
||||
},
|
||||
onAnalysisReady = { analysis ->
|
||||
diagnostics = analysis.diagnostics
|
||||
},
|
||||
onCompletionRequested = { offset, items ->
|
||||
completionOffset = offset
|
||||
completionItems = items
|
||||
},
|
||||
onDefinitionResolved = { _, target ->
|
||||
definitionTarget = target
|
||||
},
|
||||
onUsagesResolved = { _, ranges ->
|
||||
usageRanges = ranges
|
||||
},
|
||||
onDocRequested = { _, info ->
|
||||
docInfo = info
|
||||
},
|
||||
// Keep current initial size but allow the editor to grow with content
|
||||
autoGrow = true
|
||||
)
|
||||
@@ -218,10 +259,150 @@ fun TryLyngPage(route: String) {
|
||||
}
|
||||
}
|
||||
|
||||
// Language tools quick view
|
||||
Div({ classes("card", "mb-3") }) {
|
||||
Div({ classes("card-header", "d-flex", "align-items-center", "gap-2") }) {
|
||||
I({ classes("bi", "bi-diagram-3") })
|
||||
Span({ classes("fw-semibold") }) { Text("Language tools") }
|
||||
}
|
||||
Div({ classes("card-body") }) {
|
||||
Div({ classes("mb-3") }) {
|
||||
Span({ classes("fw-semibold", "me-2") }) { Text("Diagnostics") }
|
||||
if (diagnostics.isEmpty()) {
|
||||
Span({ classes("text-muted") }) { Text("No errors or warnings.") }
|
||||
} else {
|
||||
Ul({ classes("mb-0") }) {
|
||||
diagnostics.forEach { d ->
|
||||
Li {
|
||||
val sev = when (d.severity) {
|
||||
LyngDiagnosticSeverity.Error -> "Error"
|
||||
LyngDiagnosticSeverity.Warning -> "Warning"
|
||||
}
|
||||
val range = d.range?.let { " @${it.start}-${it.endExclusive}" } ?: ""
|
||||
Text("$sev: ${d.message}$range")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Div({ classes("mb-3") }) {
|
||||
Span({ classes("fw-semibold", "me-2") }) { Text("Quick docs") }
|
||||
if (docInfo == null) {
|
||||
Span({ classes("text-muted") }) { Text("Press Ctrl+Q (or ⌘+Q) on a symbol.") }
|
||||
} else {
|
||||
val info = docInfo!!
|
||||
Div({ classes("small") }) {
|
||||
Text("${info.target.kind} ${info.target.name}")
|
||||
info.signature?.let { sig ->
|
||||
Br()
|
||||
Code { Text(sig) }
|
||||
}
|
||||
info.doc?.summary?.let { doc ->
|
||||
Br()
|
||||
Text(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Div({ classes("mb-3") }) {
|
||||
Span({ classes("fw-semibold", "me-2") }) { Text("Definition") }
|
||||
if (definitionTarget == null) {
|
||||
Span({ classes("text-muted") }) { Text("Press Ctrl+B (or ⌘+B) on a symbol.") }
|
||||
} else {
|
||||
val def = definitionTarget!!
|
||||
Span({ classes("small") }) {
|
||||
Text("${def.kind} ${def.name} @${def.range.start}-${def.range.endExclusive}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Div({ classes("mb-3") }) {
|
||||
Span({ classes("fw-semibold", "me-2") }) { Text("Usages") }
|
||||
if (usageRanges.isEmpty()) {
|
||||
Span({ classes("text-muted") }) { Text("Press Ctrl+Shift+U (or ⌘+Shift+U) on a symbol.") }
|
||||
} else {
|
||||
Span({ classes("small") }) { Text("${usageRanges.size} usage(s) found.") }
|
||||
}
|
||||
}
|
||||
|
||||
Div({ classes("mb-0") }) {
|
||||
Span({ classes("fw-semibold", "me-2") }) { Text("Completions") }
|
||||
if (completionItems.isEmpty()) {
|
||||
Span({ classes("text-muted") }) { Text("Press Ctrl+Space (or ⌘+Space).") }
|
||||
} else {
|
||||
val shown = completionItems.take(8)
|
||||
Span({ classes("text-muted", "small", "ms-1") }) {
|
||||
completionOffset?.let { Text("@$it") }
|
||||
}
|
||||
Ul({ classes("mb-0") }) {
|
||||
shown.forEach { item ->
|
||||
Li { Text("${item.name} (${item.kind})") }
|
||||
}
|
||||
}
|
||||
if (completionItems.size > shown.size) {
|
||||
Span({ classes("text-muted", "small") }) {
|
||||
Text("…and ${completionItems.size - shown.size} more")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disassembly
|
||||
Div({ classes("card", "mb-3") }) {
|
||||
Div({ classes("card-header", "d-flex", "align-items-center", "gap-2") }) {
|
||||
I({ classes("bi", "bi-braces") })
|
||||
Span({ classes("fw-semibold") }) { Text("Disassembly") }
|
||||
}
|
||||
Div({ classes("card-body") }) {
|
||||
Div({ classes("d-flex", "gap-2", "align-items-center", "mb-2") }) {
|
||||
Input(type = InputType.Text, attrs = {
|
||||
classes("form-control")
|
||||
attr("placeholder", "Symbol (e.g., MyClass.method or topLevelFun)")
|
||||
value(disasmSymbol)
|
||||
onInput { ev ->
|
||||
disasmSymbol = ev.value
|
||||
}
|
||||
})
|
||||
Button(attrs = {
|
||||
classes("btn", "btn-outline-primary")
|
||||
if (disasmSymbol.isBlank()) attr("disabled", "disabled")
|
||||
onClick {
|
||||
it.preventDefault()
|
||||
val symbol = disasmSymbol.trim()
|
||||
if (symbol.isEmpty()) return@onClick
|
||||
disasmOutput = null
|
||||
disasmError = null
|
||||
scope.launch {
|
||||
try {
|
||||
disasmOutput = LyngWebTools.disassembleSymbol(code, symbol)
|
||||
} catch (t: Throwable) {
|
||||
disasmError = t.message ?: t.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}) { Text("Disassemble") }
|
||||
}
|
||||
if (disasmError != null) {
|
||||
Div({ classes("alert", "alert-danger", "py-2", "mb-2") }) { Text(disasmError!!) }
|
||||
}
|
||||
if (disasmOutput != null) {
|
||||
Pre({ classes("mb-0") }) { Code { Text(disasmOutput!!) } }
|
||||
} else if (disasmError == null) {
|
||||
Span({ classes("text-muted", "small") }) {
|
||||
Text("Uses the bytecode compiler; not a dry run.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tips
|
||||
P({ classes("text-muted", "small") }) {
|
||||
I({ classes("bi", "bi-info-circle", "me-1") })
|
||||
Text("Tip: press Ctrl+Enter (or ⌘+Enter on Mac) to run.")
|
||||
Text("Tip: Ctrl+Enter runs, Ctrl+Space completes, Ctrl+B jumps to definition, Ctrl+Shift+U finds usages, Ctrl+Q shows docs.")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user