language tools and site suport

This commit is contained in:
Sergey Chernov 2026-02-07 05:47:18 +03:00
parent 78d8e546d5
commit c16c0d7ebd
16 changed files with 1402 additions and 338 deletions

View File

@ -25,15 +25,13 @@ import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import net.sergeych.lyng.Source
import net.sergeych.lyng.binding.Binder
import net.sergeych.lyng.binding.SymbolKind
import net.sergeych.lyng.highlight.HighlightKind import net.sergeych.lyng.highlight.HighlightKind
import net.sergeych.lyng.highlight.SimpleLyngHighlighter
import net.sergeych.lyng.highlight.offsetOf import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.idea.highlight.LyngHighlighterColors import net.sergeych.lyng.idea.highlight.LyngHighlighterColors
import net.sergeych.lyng.idea.util.LyngAstManager import net.sergeych.lyng.idea.util.LyngAstManager
import net.sergeych.lyng.miniast.* import net.sergeych.lyng.tools.LyngDiagnosticSeverity
import net.sergeych.lyng.tools.LyngLanguageTools
import net.sergeych.lyng.tools.LyngSemanticKind
/** /**
* ExternalAnnotator that runs Lyng MiniAst on the document text in background * ExternalAnnotator that runs Lyng MiniAst on the document text in background
@ -43,8 +41,8 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
data class Input(val text: String, val modStamp: Long, val previousSpans: List<Span>?, val file: PsiFile) data class Input(val text: String, val modStamp: Long, val previousSpans: List<Span>?, val file: PsiFile)
data class Span(val start: Int, val end: Int, val key: com.intellij.openapi.editor.colors.TextAttributesKey) data class Span(val start: Int, val end: Int, val key: com.intellij.openapi.editor.colors.TextAttributesKey)
data class Error(val start: Int, val end: Int, val message: String) data class Diag(val start: Int, val end: Int, val message: String, val severity: HighlightSeverity)
data class Result(val modStamp: Long, val spans: List<Span>, val error: Error? = null) data class Result(val modStamp: Long, val spans: List<Span>, val diagnostics: List<Diag> = emptyList())
override fun collectInformation(file: PsiFile): Input? { override fun collectInformation(file: PsiFile): Input? {
val doc: Document = file.viewProvider.document ?: return null val doc: Document = file.viewProvider.document ?: return null
@ -59,224 +57,46 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
if (collectedInfo == null) return null if (collectedInfo == null) return null
ProgressManager.checkCanceled() ProgressManager.checkCanceled()
val text = collectedInfo.text val text = collectedInfo.text
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() } val analysis = LyngAstManager.getAnalysis(collectedInfo.file)
// Use LyngAstManager to get the (potentially merged) Mini-AST
val mini = LyngAstManager.getMiniAst(collectedInfo.file)
?: return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList()) ?: return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList())
val mini = analysis.mini
ProgressManager.checkCanceled()
val source = Source(collectedInfo.file.name, text)
val out = ArrayList<Span>(256)
fun isFollowedByParenOrBlock(rangeEnd: Int): Boolean { ProgressManager.checkCanceled()
var i = rangeEnd
while (i < text.length) { val out = ArrayList<Span>(256)
val ch = text[i] val diags = ArrayList<Diag>()
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') { i++; continue }
return ch == '(' || ch == '{'
}
return false
}
fun putRange(start: Int, end: Int, key: com.intellij.openapi.editor.colors.TextAttributesKey) { fun putRange(start: Int, end: Int, key: com.intellij.openapi.editor.colors.TextAttributesKey) {
if (start in 0..end && end <= text.length && start < end) out += Span(start, end, key) if (start in 0..end && end <= text.length && start < end) out += Span(start, end, key)
} }
fun putName(startPos: net.sergeych.lyng.Pos, name: String, key: com.intellij.openapi.editor.colors.TextAttributesKey) {
val s = source.offsetOf(startPos) fun keyForKind(kind: LyngSemanticKind): com.intellij.openapi.editor.colors.TextAttributesKey? = when (kind) {
putRange(s, (s + name.length).coerceAtMost(text.length), key) LyngSemanticKind.Function -> LyngHighlighterColors.FUNCTION
} LyngSemanticKind.Class, LyngSemanticKind.Enum, LyngSemanticKind.TypeAlias -> LyngHighlighterColors.TYPE
fun putMiniRange(r: MiniRange, key: com.intellij.openapi.editor.colors.TextAttributesKey) { LyngSemanticKind.Value -> LyngHighlighterColors.VALUE
val s = source.offsetOf(r.start) LyngSemanticKind.Variable -> LyngHighlighterColors.VARIABLE
val e = source.offsetOf(r.end) LyngSemanticKind.Parameter -> LyngHighlighterColors.PARAMETER
putRange(s, e, key) LyngSemanticKind.TypeRef -> LyngHighlighterColors.TYPE
LyngSemanticKind.EnumConstant -> LyngHighlighterColors.ENUM_CONSTANT
} }
// Declarations // Semantic highlights from shared tooling
mini.declarations.forEach { d -> LyngLanguageTools.semanticHighlights(analysis).forEach { span ->
if (d.nameStart.source != source) return@forEach keyForKind(span.kind)?.let { putRange(span.range.start, span.range.endExclusive, it) }
when (d) {
is MiniFunDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.FUNCTION_DECLARATION)
is MiniClassDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.TYPE)
is MiniValDecl -> putName(
d.nameStart,
d.name,
if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE
)
is MiniEnumDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.TYPE)
}
} }
// Imports: each segment as namespace/path // Imports: each segment as namespace/path
mini.imports.forEach { imp -> mini?.imports?.forEach { imp ->
if (imp.range.start.source != source) return@forEach imp.segments.forEach { seg ->
imp.segments.forEach { seg -> putMiniRange(seg.range, LyngHighlighterColors.NAMESPACE) } val start = analysis.source.offsetOf(seg.range.start)
} val end = analysis.source.offsetOf(seg.range.end)
putRange(start, end, LyngHighlighterColors.NAMESPACE)
// Parameters
fun addParams(params: List<MiniParam>) {
params.forEach { p ->
if (p.nameStart.source == source)
putName(p.nameStart, p.name, LyngHighlighterColors.PARAMETER)
} }
} }
mini.declarations.forEach { d ->
when (d) {
is MiniFunDecl -> addParams(d.params)
is MiniClassDecl -> d.members.filterIsInstance<MiniMemberFunDecl>().forEach { addParams(it.params) }
else -> {}
}
}
// Type name segments (including generics base & args)
fun addTypeSegments(t: MiniTypeRef?) {
when (t) {
is MiniTypeName -> t.segments.forEach { seg ->
if (seg.range.start.source != source) return@forEach
val s = source.offsetOf(seg.range.start)
putRange(s, (s + seg.name.length).coerceAtMost(text.length), LyngHighlighterColors.TYPE)
}
is MiniGenericType -> {
addTypeSegments(t.base)
t.args.forEach { addTypeSegments(it) }
}
is MiniFunctionType -> {
t.receiver?.let { addTypeSegments(it) }
t.params.forEach { addTypeSegments(it) }
addTypeSegments(t.returnType)
}
is MiniTypeVar -> { /* name is in range; could be highlighted as TYPE as well */
if (t.range.start.source == source)
putMiniRange(t.range, LyngHighlighterColors.TYPE)
}
null -> {}
}
}
fun addDeclTypeSegments(d: MiniDecl) {
if (d.nameStart.source != source) return
when (d) {
is MiniFunDecl -> {
addTypeSegments(d.returnType)
d.params.forEach { addTypeSegments(it.type) }
addTypeSegments(d.receiver)
}
is MiniValDecl -> {
addTypeSegments(d.type)
addTypeSegments(d.receiver)
}
is MiniClassDecl -> {
d.ctorFields.forEach { addTypeSegments(it.type) }
d.classFields.forEach { addTypeSegments(it.type) }
for (m in d.members) {
when (m) {
is MiniMemberFunDecl -> {
addTypeSegments(m.returnType)
m.params.forEach { addTypeSegments(it.type) }
}
is MiniMemberValDecl -> {
addTypeSegments(m.type)
}
else -> {}
}
}
}
is MiniEnumDecl -> {}
}
}
mini.declarations.forEach { d -> addDeclTypeSegments(d) }
ProgressManager.checkCanceled()
// Semantic usages via Binder (best-effort)
try {
val binding = Binder.bind(text, mini)
// Map declaration ranges to avoid duplicating them as usages
val declKeys = HashSet<Pair<Int, Int>>(binding.symbols.size * 2)
binding.symbols.forEach { sym -> declKeys += (sym.declStart to sym.declEnd) }
fun keyForKind(k: SymbolKind) = when (k) {
SymbolKind.Function -> LyngHighlighterColors.FUNCTION
SymbolKind.Class, SymbolKind.Enum -> LyngHighlighterColors.TYPE
SymbolKind.Parameter -> LyngHighlighterColors.PARAMETER
SymbolKind.Value -> LyngHighlighterColors.VALUE
SymbolKind.Variable -> LyngHighlighterColors.VARIABLE
}
// Track covered ranges to not override later heuristics
val covered = HashSet<Pair<Int, Int>>()
binding.references.forEach { ref ->
val key = ref.start to ref.end
if (!declKeys.contains(key)) {
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
if (sym != null) {
val color = keyForKind(sym.kind)
putRange(ref.start, ref.end, color)
covered += key
}
}
}
// Heuristics on top of binder: function call-sites and simple name-based roles
ProgressManager.checkCanceled()
// Build simple name -> role map for top-level vals/vars and parameters
val nameRole = HashMap<String, com.intellij.openapi.editor.colors.TextAttributesKey>(8)
mini.declarations.forEach { d ->
when (d) {
is MiniValDecl -> nameRole[d.name] =
if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE
is MiniFunDecl -> d.params.forEach { p -> nameRole[p.name] = LyngHighlighterColors.PARAMETER }
is MiniClassDecl -> {
d.members.forEach { m ->
if (m is MiniMemberFunDecl) {
m.params.forEach { p -> nameRole[p.name] = LyngHighlighterColors.PARAMETER }
}
}
}
else -> {}
}
}
tokens.forEach { s ->
if (s.kind == HighlightKind.Identifier) {
val start = s.range.start
val end = s.range.endExclusive
val key = start to end
if (key !in covered && key !in declKeys) {
// Call-site detection first so it wins over var/param role
if (isFollowedByParenOrBlock(end)) {
putRange(start, end, LyngHighlighterColors.FUNCTION)
covered += key
} else {
// Simple role by known names
val ident = try {
text.substring(start, end)
} catch (_: Throwable) {
null
}
if (ident != null) {
val roleKey = nameRole[ident]
if (roleKey != null) {
putRange(start, end, roleKey)
covered += key
}
}
}
}
}
}
} catch (e: Throwable) {
// Must rethrow cancellation; otherwise ignore binder failures (best-effort)
if (e is com.intellij.openapi.progress.ProcessCanceledException) throw e
}
// Add annotation/label coloring using token highlighter // Add annotation/label coloring using token highlighter
run { run {
tokens.forEach { s -> analysis.lexicalHighlights.forEach { s ->
if (s.kind == HighlightKind.Label) { if (s.kind == HighlightKind.Label) {
val start = s.range.start val start = s.range.start
val end = s.range.endExclusive val end = s.range.endExclusive
@ -302,7 +122,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
text.substring(wStart, wEnd) text.substring(wStart, wEnd)
} else null } else null
if (prevWord in setOf("return", "break", "continue") || isFollowedByParenOrBlock(end)) { if (prevWord in setOf("return", "break", "continue")) {
putRange(start, end, LyngHighlighterColors.LABEL) putRange(start, end, LyngHighlighterColors.LABEL)
} else { } else {
putRange(start, end, LyngHighlighterColors.ANNOTATION) putRange(start, end, LyngHighlighterColors.ANNOTATION)
@ -315,17 +135,13 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
} }
} }
tokens.forEach { s -> analysis.diagnostics.forEach { d ->
if (s.kind == HighlightKind.EnumConstant) { val range = d.range ?: return@forEach
val start = s.range.start val severity = if (d.severity == LyngDiagnosticSeverity.Warning) HighlightSeverity.WARNING else HighlightSeverity.ERROR
val end = s.range.endExclusive diags += Diag(range.start, range.endExclusive, d.message, severity)
if (start in 0..end && end <= text.length && start < end) {
putRange(start, end, LyngHighlighterColors.ENUM_CONSTANT)
}
}
} }
return Result(collectedInfo.modStamp, out, null) return Result(collectedInfo.modStamp, out, diags)
} }
@ -346,13 +162,12 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
.create() .create()
} }
// Show syntax error if present // Show errors and warnings
val err = result.error result.diagnostics.forEach { d ->
if (err != null) { val start = d.start.coerceIn(0, (doc?.textLength ?: 0))
val start = err.start.coerceIn(0, (doc?.textLength ?: 0)) val end = d.end.coerceIn(start, (doc?.textLength ?: start))
val end = err.end.coerceIn(start, (doc?.textLength ?: start))
if (end > start) { if (end > start) {
holder.newAnnotation(HighlightSeverity.ERROR, err.message) holder.newAnnotation(d.severity, d.message)
.range(TextRange(start, end)) .range(TextRange(start, end))
.create() .create()
} }
@ -373,30 +188,5 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
return -1 return -1
} }
/**
* Make the error highlight a bit wider than a single character so it is easier to see and click.
* Strategy:
* - If the offset points inside an identifier-like token (letters/digits/underscore), expand to the full token.
* - Otherwise select a small range starting at the offset with a minimum width, but not crossing the line end.
*/
private fun expandErrorRange(text: String, rawStart: Int): Pair<Int, Int> {
if (text.isEmpty()) return 0 to 0
val len = text.length
val start = rawStart.coerceIn(0, len)
fun isWord(ch: Char) = ch == '_' || ch.isLetterOrDigit()
if (start < len && isWord(text[start])) {
var s = start
var e = start
while (s > 0 && isWord(text[s - 1])) s--
while (e < len && isWord(text[e])) e++
return s to e
}
// Not inside a word: select a short, visible range up to EOL
val lineEnd = text.indexOf('\n', start).let { if (it == -1) len else it }
val minWidth = 4
val end = (start + minWidth).coerceAtMost(lineEnd).coerceAtLeast((start + 1).coerceAtMost(lineEnd))
return start to end
}
} }

View File

@ -96,9 +96,10 @@ class LyngCompletionContributor : CompletionContributor() {
log.info("[LYNG_DEBUG] Completion: caret=$caret prefix='${prefix}' memberDotPos=${memberDotPos} file='${file.name}'") log.info("[LYNG_DEBUG] Completion: caret=$caret prefix='${prefix}' memberDotPos=${memberDotPos} file='${file.name}'")
} }
// Build MiniAst (cached) for both global and member contexts to enable local class/val inference // Build analysis (cached) for both global and member contexts to enable local class/val inference
val mini = LyngAstManager.getMiniAst(file) val analysis = LyngAstManager.getAnalysis(file)
val binding = LyngAstManager.getBinding(file) val mini = analysis?.mini
val binding = analysis?.binding
// Delegate computation to the shared engine to keep behavior in sync with tests // Delegate computation to the shared engine to keep behavior in sync with tests
val engineItems = try { val engineItems = try {
@ -160,6 +161,8 @@ class LyngCompletionContributor : CompletionContributor() {
.withIcon(AllIcons.Nodes.Class) .withIcon(AllIcons.Nodes.Class)
Kind.Enum -> LookupElementBuilder.create(ci.name) Kind.Enum -> LookupElementBuilder.create(ci.name)
.withIcon(AllIcons.Nodes.Enum) .withIcon(AllIcons.Nodes.Enum)
Kind.TypeAlias -> LookupElementBuilder.create(ci.name)
.withIcon(AllIcons.Nodes.Class)
Kind.Value -> LookupElementBuilder.create(ci.name) Kind.Value -> LookupElementBuilder.create(ci.name)
.withIcon(AllIcons.Nodes.Variable) .withIcon(AllIcons.Nodes.Variable)
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b } .let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }

View File

@ -29,6 +29,8 @@ import net.sergeych.lyng.idea.LyngLanguage
import net.sergeych.lyng.idea.util.LyngAstManager import net.sergeych.lyng.idea.util.LyngAstManager
import net.sergeych.lyng.idea.util.TextCtx import net.sergeych.lyng.idea.util.TextCtx
import net.sergeych.lyng.miniast.* import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.tools.LyngLanguageTools
import net.sergeych.lyng.tools.LyngSymbolInfo
/** /**
* Quick Docs backed by MiniAst: when caret is on an identifier that corresponds * Quick Docs backed by MiniAst: when caret is on an identifier that corresponds
@ -66,9 +68,15 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: ident='$ident' at ${idRange.startOffset}..${idRange.endOffset} in ${file.name}") if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: ident='$ident' at ${idRange.startOffset}..${idRange.endOffset} in ${file.name}")
// 1. Get merged mini-AST from Manager (handles local + .lyng.d merged declarations) // 1. Get merged mini-AST from Manager (handles local + .lyng.d merged declarations)
val mini = LyngAstManager.getMiniAst(file) ?: return null val analysis = LyngAstManager.getAnalysis(file) ?: return null
val mini = analysis.mini ?: return null
val miniSource = mini.range.start.source val miniSource = mini.range.start.source
val imported = DocLookupUtils.canonicalImportedModules(mini, text) val imported = analysis.importedModules.ifEmpty { DocLookupUtils.canonicalImportedModules(mini, text) }
// Single-source quick doc lookup
LyngLanguageTools.docAt(analysis, offset)?.let { info ->
renderDocFromInfo(info)?.let { return it }
}
// Try resolve to: function param at position, function/class/val declaration at position // Try resolve to: function param at position, function/class/val declaration at position
// 1) Use unified declaration detection // 1) Use unified declaration detection
@ -91,6 +99,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return when (m) { return when (m) {
is MiniMemberFunDecl -> renderMemberFunDoc(d.name, m) is MiniMemberFunDecl -> renderMemberFunDoc(d.name, m)
is MiniMemberValDecl -> renderMemberValDoc(d.name, m) is MiniMemberValDecl -> renderMemberValDoc(d.name, m)
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(d.name, m)
else -> null else -> null
} }
} }
@ -197,6 +206,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return when (m) { return when (m) {
is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m) is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m)
is MiniMemberValDecl -> renderMemberValDoc(cls.name, m) is MiniMemberValDecl -> renderMemberValDoc(cls.name, m)
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(cls.name, m)
else -> null else -> null
} }
} }
@ -312,11 +322,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return when (member) { return when (member) {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
is MiniInitDecl -> null is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules)
} }
} }
log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}") log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}")
@ -354,6 +366,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
// And classes/enums // And classes/enums
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) } docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
docs.filterIsInstance<MiniEnumDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) } docs.filterIsInstance<MiniEnumDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
docs.filterIsInstance<MiniTypeAliasDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
} }
// Defensive fallback: if nothing found and it's a well-known stdlib function, render minimal inline docs // Defensive fallback: if nothing found and it's a well-known stdlib function, render minimal inline docs
if (ident == "println" || ident == "print") { if (ident == "println" || ident == "print") {
@ -372,11 +385,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return when (member) { return when (member) {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
is MiniInitDecl -> null is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules)
} }
} }
} else { } else {
@ -395,11 +410,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return when (member) { return when (member) {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
is MiniInitDecl -> null is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules)
} }
} }
} else { } else {
@ -412,11 +429,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return when (member) { return when (member) {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
is MiniInitDecl -> null is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules)
} }
} }
} }
@ -431,6 +450,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return when (m) { return when (m) {
is MiniMemberFunDecl -> renderMemberFunDoc("String", m) is MiniMemberFunDecl -> renderMemberFunDoc("String", m)
is MiniMemberValDecl -> renderMemberValDoc("String", m) is MiniMemberValDecl -> renderMemberValDoc("String", m)
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc("String", m)
is MiniInitDecl -> null is MiniInitDecl -> null
} }
} }
@ -512,6 +532,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
is MiniFunDecl -> "function ${d.name}${signatureOf(d)}" is MiniFunDecl -> "function ${d.name}${signatureOf(d)}"
is MiniClassDecl -> "class ${d.name}" is MiniClassDecl -> "class ${d.name}"
is MiniEnumDecl -> "enum ${d.name} { ${d.entries.joinToString(", ")} }" is MiniEnumDecl -> "enum ${d.name} { ${d.entries.joinToString(", ")} }"
is MiniTypeAliasDecl -> "type ${d.name}${typeAliasSuffix(d)}"
is MiniValDecl -> { is MiniValDecl -> {
val t = d.type ?: DocLookupUtils.inferTypeRefForVal(d, text, imported, mini) val t = d.type ?: DocLookupUtils.inferTypeRefForVal(d, text, imported, mini)
val typeStr = if (t == null) ": Object?" else typeOf(t) val typeStr = if (t == null) ": Object?" else typeOf(t)
@ -524,6 +545,24 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return sb.toString() return sb.toString()
} }
private fun renderDocFromInfo(info: LyngSymbolInfo): String? {
val kind = when (info.target.kind) {
net.sergeych.lyng.binding.SymbolKind.Function -> "function"
net.sergeych.lyng.binding.SymbolKind.Class -> "class"
net.sergeych.lyng.binding.SymbolKind.Enum -> "enum"
net.sergeych.lyng.binding.SymbolKind.TypeAlias -> "type"
net.sergeych.lyng.binding.SymbolKind.Value -> "val"
net.sergeych.lyng.binding.SymbolKind.Variable -> "var"
net.sergeych.lyng.binding.SymbolKind.Parameter -> "parameter"
}
val title = info.signature ?: "$kind ${info.target.name}"
if (title.isBlank() && info.doc == null) return null
val sb = StringBuilder()
sb.append(renderTitle(title))
sb.append(renderDocBody(info.doc))
return sb.toString()
}
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String { private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}" val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
val sb = StringBuilder() val sb = StringBuilder()
@ -565,6 +604,25 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return sb.toString() return sb.toString()
} }
private fun renderMemberTypeAliasDoc(className: String, m: MiniMemberTypeAliasDecl): String {
val tp = if (m.typeParams.isEmpty()) "" else "<" + m.typeParams.joinToString(", ") + ">"
val body = typeOf(m.target)
val rhs = if (body.isBlank()) "" else " = ${body.removePrefix(": ")}"
val staticStr = if (m.isStatic) "static " else ""
val title = "${staticStr}type $className.${m.name}$tp$rhs"
val sb = StringBuilder()
sb.append(renderTitle(title))
sb.append(renderDocBody(m.doc))
return sb.toString()
}
private fun typeAliasSuffix(d: MiniTypeAliasDecl): String {
val tp = if (d.typeParams.isEmpty()) "" else "<" + d.typeParams.joinToString(", ") + ">"
val body = typeOf(d.target)
val rhs = if (body.isBlank()) "" else " = ${body.removePrefix(": ")}"
return "$tp$rhs"
}
private fun typeOf(t: MiniTypeRef?): String { private fun typeOf(t: MiniTypeRef?): String {
val s = DocLookupUtils.typeOf(t) val s = DocLookupUtils.typeOf(t)
return if (s.isEmpty()) (if (t == null) ": Object?" else "") else ": $s" return if (s.isEmpty()) (if (t == null) ": Object?" else "") else ": $s"

View File

@ -36,9 +36,10 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
val name = element.text ?: "" val name = element.text ?: ""
val results = mutableListOf<ResolveResult>() val results = mutableListOf<ResolveResult>()
val mini = LyngAstManager.getMiniAst(file) ?: return emptyArray() val analysis = LyngAstManager.getAnalysis(file) ?: return emptyArray()
val binding = LyngAstManager.getBinding(file) val mini = analysis.mini ?: return emptyArray()
val imported = DocLookupUtils.canonicalImportedModules(mini, text).toSet() val binding = analysis.binding
val imported = analysis.importedModules.toSet()
val currentPackage = getPackageName(file) val currentPackage = getPackageName(file)
val allowedPackages = if (currentPackage != null) imported + currentPackage else imported val allowedPackages = if (currentPackage != null) imported + currentPackage else imported
@ -64,11 +65,13 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
val kind = when(member) { val kind = when(member) {
is MiniMemberFunDecl -> "Function" is MiniMemberFunDecl -> "Function"
is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value" is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value"
is MiniMemberTypeAliasDecl -> "TypeAlias"
is MiniInitDecl -> "Initializer" is MiniInitDecl -> "Initializer"
is MiniFunDecl -> "Function" is MiniFunDecl -> "Function"
is MiniValDecl -> if (member.mutable) "Variable" else "Value" is MiniValDecl -> if (member.mutable) "Variable" else "Value"
is MiniClassDecl -> "Class" is MiniClassDecl -> "Class"
is MiniEnumDecl -> "Enum" is MiniEnumDecl -> "Enum"
is MiniTypeAliasDecl -> "TypeAlias"
} }
results.add(PsiElementResolveResult(LyngDeclarationElement(it, member.name, kind))) results.add(PsiElementResolveResult(LyngDeclarationElement(it, member.name, kind)))
} }
@ -199,6 +202,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
is net.sergeych.lyng.miniast.MiniClassDecl -> "Class" is net.sergeych.lyng.miniast.MiniClassDecl -> "Class"
is net.sergeych.lyng.miniast.MiniEnumDecl -> "Enum" is net.sergeych.lyng.miniast.MiniEnumDecl -> "Enum"
is net.sergeych.lyng.miniast.MiniValDecl -> if (d.mutable) "Variable" else "Value" is net.sergeych.lyng.miniast.MiniValDecl -> if (d.mutable) "Variable" else "Value"
is net.sergeych.lyng.miniast.MiniTypeAliasDecl -> "TypeAlias"
} }
addIfMatch(d.name, d.nameStart, dKind) addIfMatch(d.name, d.nameStart, dKind)
} }
@ -214,6 +218,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
val mKind = when(m) { val mKind = when(m) {
is net.sergeych.lyng.miniast.MiniMemberFunDecl -> "Function" is net.sergeych.lyng.miniast.MiniMemberFunDecl -> "Function"
is net.sergeych.lyng.miniast.MiniMemberValDecl -> if (m.mutable) "Variable" else "Value" is net.sergeych.lyng.miniast.MiniMemberValDecl -> if (m.mutable) "Variable" else "Value"
is net.sergeych.lyng.miniast.MiniMemberTypeAliasDecl -> "TypeAlias"
is net.sergeych.lyng.miniast.MiniInitDecl -> "Initializer" is net.sergeych.lyng.miniast.MiniInitDecl -> "Initializer"
} }
addIfMatch(m.name, m.nameStart, mKind) addIfMatch(m.name, m.nameStart, mKind)

View File

@ -22,55 +22,21 @@ import com.intellij.openapi.util.Key
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager import com.intellij.psi.PsiManager
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Source
import net.sergeych.lyng.binding.Binder
import net.sergeych.lyng.binding.BindingSnapshot import net.sergeych.lyng.binding.BindingSnapshot
import net.sergeych.lyng.miniast.MiniAstBuilder import net.sergeych.lyng.miniast.DocLookupUtils
import net.sergeych.lyng.miniast.MiniScript import net.sergeych.lyng.miniast.MiniScript
import net.sergeych.lyng.tools.LyngAnalysisRequest
import net.sergeych.lyng.tools.LyngAnalysisResult
import net.sergeych.lyng.tools.LyngLanguageTools
object LyngAstManager { object LyngAstManager {
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache") private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
private val BINDING_KEY = Key.create<BindingSnapshot>("lyng.binding.cache") private val BINDING_KEY = Key.create<BindingSnapshot>("lyng.binding.cache")
private val STAMP_KEY = Key.create<Long>("lyng.mini.cache.stamp") private val STAMP_KEY = Key.create<Long>("lyng.mini.cache.stamp")
private val ANALYSIS_KEY = Key.create<LyngAnalysisResult>("lyng.analysis.cache")
fun getMiniAst(file: PsiFile): MiniScript? = runReadAction { fun getMiniAst(file: PsiFile): MiniScript? = runReadAction {
val vFile = file.virtualFile ?: return@runReadAction null getAnalysis(file)?.mini
val combinedStamp = getCombinedStamp(file)
val prevStamp = file.getUserData(STAMP_KEY)
val cached = file.getUserData(MINI_KEY)
if (cached != null && prevStamp != null && prevStamp == combinedStamp) return@runReadAction cached
val text = file.viewProvider.contents.toString()
val sink = MiniAstBuilder()
val built = try {
val provider = IdeLenientImportProvider.create()
val src = Source(file.name, text)
runBlocking { Compiler.compileWithMini(src, provider, sink) }
val script = sink.build()
if (script != null && !file.name.endsWith(".lyng.d")) {
val dFiles = collectDeclarationFiles(file)
for (df in dFiles) {
val scriptD = getMiniAst(df)
if (scriptD != null) {
script.declarations.addAll(scriptD.declarations)
script.imports.addAll(scriptD.imports)
}
}
}
script
} catch (_: Throwable) {
sink.build()
}
if (built != null) {
file.putUserData(MINI_KEY, built)
file.putUserData(STAMP_KEY, combinedStamp)
// Invalidate binding too
file.putUserData(BINDING_KEY, null)
}
built
} }
fun getCombinedStamp(file: PsiFile): Long = runReadAction { fun getCombinedStamp(file: PsiFile): Long = runReadAction {
@ -102,32 +68,53 @@ object LyngAstManager {
} }
fun getBinding(file: PsiFile): BindingSnapshot? = runReadAction { fun getBinding(file: PsiFile): BindingSnapshot? = runReadAction {
getAnalysis(file)?.binding
}
fun getAnalysis(file: PsiFile): LyngAnalysisResult? = runReadAction {
val vFile = file.virtualFile ?: return@runReadAction null val vFile = file.virtualFile ?: return@runReadAction null
var combinedStamp = file.viewProvider.modificationStamp val combinedStamp = getCombinedStamp(file)
val dFiles = if (!file.name.endsWith(".lyng.d")) collectDeclarationFiles(file) else emptyList()
for (df in dFiles) {
combinedStamp += df.viewProvider.modificationStamp
}
val prevStamp = file.getUserData(STAMP_KEY) val prevStamp = file.getUserData(STAMP_KEY)
val cached = file.getUserData(BINDING_KEY) val cached = file.getUserData(ANALYSIS_KEY)
if (cached != null && prevStamp != null && prevStamp == combinedStamp) return@runReadAction cached if (cached != null && prevStamp != null && prevStamp == combinedStamp) return@runReadAction cached
val mini = getMiniAst(file) ?: return@runReadAction null
val text = file.viewProvider.contents.toString() val text = file.viewProvider.contents.toString()
val binding = try { val built = try {
Binder.bind(text, mini) val provider = IdeLenientImportProvider.create()
runBlocking {
LyngLanguageTools.analyze(
LyngAnalysisRequest(text = text, fileName = file.name, importProvider = provider)
)
}
} catch (_: Throwable) { } catch (_: Throwable) {
null null
} }
if (binding != null) { if (built != null) {
file.putUserData(BINDING_KEY, binding) val merged = built.mini
// stamp is already set by getMiniAst or we set it here if getMiniAst was cached if (merged != null && !file.name.endsWith(".lyng.d")) {
val dFiles = collectDeclarationFiles(file)
for (df in dFiles) {
val dAnalysis = getAnalysis(df)
val dMini = dAnalysis?.mini ?: continue
merged.declarations.addAll(dMini.declarations)
merged.imports.addAll(dMini.imports)
}
}
val finalAnalysis = if (merged != null) {
built.copy(
mini = merged,
importedModules = DocLookupUtils.canonicalImportedModules(merged, text)
)
} else {
built
}
file.putUserData(ANALYSIS_KEY, finalAnalysis)
file.putUserData(MINI_KEY, finalAnalysis.mini)
file.putUserData(BINDING_KEY, finalAnalysis.binding)
file.putUserData(STAMP_KEY, combinedStamp) file.putUserData(STAMP_KEY, combinedStamp)
return@runReadAction finalAnalysis
} }
binding null
} }
} }

View File

@ -27,7 +27,7 @@ import net.sergeych.lyng.highlight.SimpleLyngHighlighter
import net.sergeych.lyng.highlight.offsetOf import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.miniast.* import net.sergeych.lyng.miniast.*
enum class SymbolKind { Class, Enum, Function, Value, Variable, Parameter } enum class SymbolKind { Class, Enum, TypeAlias, Function, Value, Variable, Parameter }
data class Symbol( data class Symbol(
val id: Int, val id: Int,
@ -126,13 +126,22 @@ object Binder {
} }
// Members (including fields and methods) // Members (including fields and methods)
for (m in d.members) { for (m in d.members) {
if (m is MiniMemberValDecl) { when (m) {
val fs = source.offsetOf(m.nameStart) is MiniMemberValDecl -> {
val fe = fs + m.name.length val fs = source.offsetOf(m.nameStart)
val kind = if (m.mutable) SymbolKind.Variable else SymbolKind.Value val fe = fs + m.name.length
val fieldSym = Symbol(nextId++, m.name, kind, fs, fe, containerId = sym.id, type = DocLookupUtils.typeOf(m.type)) val kind = if (m.mutable) SymbolKind.Variable else SymbolKind.Value
symbols += fieldSym val fieldSym = Symbol(nextId++, m.name, kind, fs, fe, containerId = sym.id, type = DocLookupUtils.typeOf(m.type))
classScope.fields += fieldSym.id symbols += fieldSym
classScope.fields += fieldSym.id
}
is MiniMemberTypeAliasDecl -> {
val fs = source.offsetOf(m.nameStart)
val fe = fs + m.name.length
val aliasSym = Symbol(nextId++, m.name, SymbolKind.TypeAlias, fs, fe, containerId = sym.id, type = DocLookupUtils.typeOf(m.target))
symbols += aliasSym
}
else -> {}
} }
} }
} }
@ -197,6 +206,12 @@ object Binder {
symbols += sym symbols += sym
topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id) topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id)
} }
is MiniTypeAliasDecl -> {
val (s, e) = nameOffsets(d.nameStart, d.name)
val sym = Symbol(nextId++, d.name, SymbolKind.TypeAlias, s, e, containerId = null, type = DocLookupUtils.typeOf(d.target))
symbols += sym
topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id)
}
} }
} }

View File

@ -36,7 +36,7 @@ data class CompletionItem(
val priority: Double = 0.0, val priority: Double = 0.0,
) )
enum class Kind { Function, Class_, Enum, Value, Method, Field } enum class Kind { Function, Class_, Enum, TypeAlias, Value, Method, Field }
/** /**
* Platform-free, lenient import provider that never fails on unknown packages. * Platform-free, lenient import provider that never fails on unknown packages.
@ -118,9 +118,11 @@ object CompletionEngineLight {
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() } val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() } val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() } val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
val aliases = decls.filterIsInstance<MiniTypeAliasDecl>().sortedBy { it.name.lowercase() }
funs.forEach { offerDeclAdd(out, prefix, it) } funs.forEach { offerDeclAdd(out, prefix, it) }
classes.forEach { offerDeclAdd(out, prefix, it) } classes.forEach { offerDeclAdd(out, prefix, it) }
enums.forEach { offerDeclAdd(out, prefix, it) } enums.forEach { offerDeclAdd(out, prefix, it) }
aliases.forEach { offerDeclAdd(out, prefix, it) }
vals.forEach { offerDeclAdd(out, prefix, it) } vals.forEach { offerDeclAdd(out, prefix, it) }
// Imported and builtin // Imported and builtin
@ -135,9 +137,11 @@ object CompletionEngineLight {
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() } val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() } val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() } val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
val aliases = decls.filterIsInstance<MiniTypeAliasDecl>().sortedBy { it.name.lowercase() }
funs.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } } funs.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
classes.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } } classes.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
enums.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } } enums.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
aliases.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
vals.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } } vals.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
if (out.size >= cap || externalAdded >= budget) break if (out.size >= cap || externalAdded >= budget) break
} }
@ -196,6 +200,9 @@ object CompletionEngineLight {
is MiniMemberValDecl -> { is MiniMemberValDecl -> {
add(CompletionItem(m.name, if (m.mutable) Kind.Value else Kind.Field, typeText = typeOf(m.type), priority = 100.0)) add(CompletionItem(m.name, if (m.mutable) Kind.Value else Kind.Field, typeText = typeOf(m.type), priority = 100.0))
} }
is MiniMemberTypeAliasDecl -> {
add(CompletionItem(m.name, Kind.TypeAlias, typeText = typeOf(m.target), priority = 100.0))
}
is MiniInitDecl -> {} is MiniInitDecl -> {}
} }
} }
@ -225,6 +232,7 @@ object CompletionEngineLight {
} }
is MiniClassDecl -> add(CompletionItem(d.name, Kind.Class_)) is MiniClassDecl -> add(CompletionItem(d.name, Kind.Class_))
is MiniEnumDecl -> add(CompletionItem(d.name, Kind.Enum)) is MiniEnumDecl -> add(CompletionItem(d.name, Kind.Enum))
is MiniTypeAliasDecl -> add(CompletionItem(d.name, Kind.TypeAlias, typeText = typeOf(d.target)))
is MiniValDecl -> add(CompletionItem(d.name, Kind.Value, typeText = typeOf(d.type))) is MiniValDecl -> add(CompletionItem(d.name, Kind.Value, typeText = typeOf(d.type)))
// else -> add(CompletionItem(d.name, Kind.Value)) // else -> add(CompletionItem(d.name, Kind.Value))
} }
@ -289,6 +297,10 @@ object CompletionEngineLight {
val ci = CompletionItem(name, Kind.Field, typeText = typeOf(chosen.type), priority = groupPriority) val ci = CompletionItem(name, Kind.Field, typeText = typeOf(chosen.type), priority = groupPriority)
if (ci.name.startsWith(prefix, true)) out += ci if (ci.name.startsWith(prefix, true)) out += ci
} }
is MiniMemberTypeAliasDecl -> {
val ci = CompletionItem(name, Kind.TypeAlias, typeText = typeOf(rep.target), priority = groupPriority)
if (ci.name.startsWith(prefix, true)) out += ci
}
is MiniInitDecl -> {} is MiniInitDecl -> {}
} }
} }
@ -317,6 +329,8 @@ object CompletionEngineLight {
} }
is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0) is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0)
is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0) is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0)
is MiniMemberTypeAliasDecl -> CompletionItem(name, Kind.TypeAlias, typeText = typeOf(m.target), priority = 50.0)
is MiniTypeAliasDecl -> CompletionItem(name, Kind.TypeAlias, typeText = typeOf(m.target), priority = 50.0)
else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null, priority = 50.0) else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null, priority = 50.0)
} }
if (ci.name.startsWith(prefix, true)) { if (ci.name.startsWith(prefix, true)) {

View File

@ -34,6 +34,7 @@ object DocLookupUtils {
is MiniFunDecl -> "Function" is MiniFunDecl -> "Function"
is MiniClassDecl -> "Class" is MiniClassDecl -> "Class"
is MiniEnumDecl -> "Enum" is MiniEnumDecl -> "Enum"
is MiniTypeAliasDecl -> "TypeAlias"
is MiniValDecl -> if (d.mutable) "Variable" else "Value" is MiniValDecl -> if (d.mutable) "Variable" else "Value"
} }
return d.name to kind return d.name to kind
@ -87,6 +88,7 @@ object DocLookupUtils {
val kind = when (m) { val kind = when (m) {
is MiniMemberFunDecl -> "Function" is MiniMemberFunDecl -> "Function"
is MiniMemberValDecl -> if (m.isStatic) "Value" else (if (m.mutable) "Variable" else "Value") is MiniMemberValDecl -> if (m.isStatic) "Value" else (if (m.mutable) "Variable" else "Value")
is MiniMemberTypeAliasDecl -> "TypeAlias"
is MiniInitDecl -> "Initializer" is MiniInitDecl -> "Initializer"
} }
return m.name to kind return m.name to kind
@ -119,6 +121,7 @@ object DocLookupUtils {
return when (d) { return when (d) {
is MiniValDecl -> d.type ?: if (text != null && imported != null) inferTypeRefForVal(d, text, imported, mini) else null is MiniValDecl -> d.type ?: if (text != null && imported != null) inferTypeRefForVal(d, text, imported, mini) else null
is MiniFunDecl -> d.returnType is MiniFunDecl -> d.returnType
is MiniTypeAliasDecl -> d.target
else -> null else -> null
} }
} }
@ -142,6 +145,7 @@ object DocLookupUtils {
is MiniMemberValDecl -> m.type ?: if (text != null && imported != null) { is MiniMemberValDecl -> m.type ?: if (text != null && imported != null) {
inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini) inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini)
} else null } else null
is MiniMemberTypeAliasDecl -> m.target
else -> null else -> null
} }
@ -436,7 +440,10 @@ object DocLookupUtils {
val type = findTypeByRange(mini, sym.name, sym.declStart, text, imported) val type = findTypeByRange(mini, sym.name, sym.declStart, text, imported)
simpleClassNameOf(type)?.let { return it } simpleClassNameOf(type)?.let { return it }
// if it's a class/enum, return its name directly // if it's a class/enum, return its name directly
if (sym.kind == net.sergeych.lyng.binding.SymbolKind.Class || sym.kind == net.sergeych.lyng.binding.SymbolKind.Enum) return sym.name if (sym.kind == net.sergeych.lyng.binding.SymbolKind.Class ||
sym.kind == net.sergeych.lyng.binding.SymbolKind.Enum ||
sym.kind == net.sergeych.lyng.binding.SymbolKind.TypeAlias
) return sym.name
} }
} }
} }
@ -451,6 +458,7 @@ object DocLookupUtils {
return when (d) { return when (d) {
is MiniClassDecl -> d.name is MiniClassDecl -> d.name
is MiniEnumDecl -> d.name is MiniEnumDecl -> d.name
is MiniTypeAliasDecl -> d.name
is MiniValDecl -> simpleClassNameOf(d.type ?: inferTypeRefForVal(d, text, imported, mini)) is MiniValDecl -> simpleClassNameOf(d.type ?: inferTypeRefForVal(d, text, imported, mini))
is MiniFunDecl -> simpleClassNameOf(d.returnType) is MiniFunDecl -> simpleClassNameOf(d.returnType)
} }
@ -565,6 +573,7 @@ object DocLookupUtils {
val rt = when (mm) { val rt = when (mm) {
is MiniMemberFunDecl -> mm.returnType is MiniMemberFunDecl -> mm.returnType
is MiniMemberValDecl -> mm.type is MiniMemberValDecl -> mm.type
is MiniMemberTypeAliasDecl -> mm.target
else -> null else -> null
} }
return simpleClassNameOf(rt) return simpleClassNameOf(rt)
@ -580,8 +589,10 @@ object DocLookupUtils {
val rt = when (m) { val rt = when (m) {
is MiniMemberFunDecl -> m.returnType is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type is MiniMemberValDecl -> m.type
is MiniMemberTypeAliasDecl -> m.target
is MiniFunDecl -> m.returnType is MiniFunDecl -> m.returnType
is MiniValDecl -> m.type is MiniValDecl -> m.type
is MiniTypeAliasDecl -> m.target
else -> null else -> null
} }
simpleClassNameOf(rt) simpleClassNameOf(rt)
@ -921,6 +932,7 @@ object DocLookupUtils {
return when (val m = resolved.second) { return when (val m = resolved.second) {
is MiniMemberFunDecl -> m.returnType is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, fullText, imported, mini) is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, fullText, imported, mini)
is MiniMemberTypeAliasDecl -> m.target
else -> null else -> null
} }
} }
@ -943,6 +955,7 @@ object DocLookupUtils {
is MiniEnumDecl -> syntheticTypeRef(d.name) is MiniEnumDecl -> syntheticTypeRef(d.name)
is MiniValDecl -> d.type ?: inferTypeRefForVal(d, fullText, imported, mini) is MiniValDecl -> d.type ?: inferTypeRefForVal(d, fullText, imported, mini)
is MiniFunDecl -> d.returnType is MiniFunDecl -> d.returnType
is MiniTypeAliasDecl -> d.target
} }
} }

View File

@ -241,6 +241,17 @@ data class MiniEnumDecl(
val entryPositions: List<Pos> = emptyList() val entryPositions: List<Pos> = emptyList()
) : MiniDecl ) : MiniDecl
data class MiniTypeAliasDecl(
override val range: MiniRange,
override val name: String,
val typeParams: List<String>,
val target: MiniTypeRef?,
override val doc: MiniDoc?,
override val nameStart: Pos,
override val isExtern: Boolean = false,
override val isStatic: Boolean = false,
) : MiniDecl
data class MiniCtorField( data class MiniCtorField(
val name: String, val name: String,
val mutable: Boolean, val mutable: Boolean,
@ -290,6 +301,17 @@ data class MiniMemberValDecl(
override val isExtern: Boolean = false, override val isExtern: Boolean = false,
) : MiniMemberDecl ) : MiniMemberDecl
data class MiniMemberTypeAliasDecl(
override val range: MiniRange,
override val name: String,
val typeParams: List<String>,
val target: MiniTypeRef?,
override val doc: MiniDoc?,
override val nameStart: Pos,
override val isStatic: Boolean = false,
override val isExtern: Boolean = false,
) : MiniMemberDecl
data class MiniInitDecl( data class MiniInitDecl(
override val range: MiniRange, override val range: MiniRange,
override val nameStart: Pos, override val nameStart: Pos,
@ -319,6 +341,7 @@ interface MiniAstSink {
fun onInitDecl(node: MiniInitDecl) {} fun onInitDecl(node: MiniInitDecl) {}
fun onClassDecl(node: MiniClassDecl) {} fun onClassDecl(node: MiniClassDecl) {}
fun onEnumDecl(node: MiniEnumDecl) {} fun onEnumDecl(node: MiniEnumDecl) {}
fun onTypeAliasDecl(node: MiniTypeAliasDecl) {}
fun onBlock(node: MiniBlock) {} fun onBlock(node: MiniBlock) {}
fun onIdentifier(node: MiniIdentifier) {} fun onIdentifier(node: MiniIdentifier) {}
@ -489,6 +512,41 @@ class MiniAstBuilder : MiniAstSink {
lastDoc = null lastDoc = null
} }
override fun onTypeAliasDecl(node: MiniTypeAliasDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc)
val currentClass = classStack.lastOrNull()
if (currentClass != null && functionDepth == 0) {
val member = MiniMemberTypeAliasDecl(
range = attach.range,
name = attach.name,
typeParams = attach.typeParams,
target = attach.target,
doc = attach.doc,
nameStart = attach.nameStart,
isStatic = attach.isStatic,
isExtern = attach.isExtern
)
val existing = currentClass.members.filterIsInstance<MiniMemberTypeAliasDecl>()
.find { it.name == attach.name && it.nameStart == attach.nameStart }
val updatedMembers = if (existing != null) {
currentClass.members.map { if (it === existing) member else it }
} else {
currentClass.members + member
}
classStack.removeLast()
classStack.addLast(currentClass.copy(members = updatedMembers))
} else {
val existing = currentScript?.declarations?.find { it.name == attach.name && it.nameStart == attach.nameStart }
if (existing != null) {
val idx = currentScript?.declarations?.indexOf(existing) ?: -1
if (idx >= 0) currentScript?.declarations?.set(idx, attach)
} else {
currentScript?.declarations?.add(attach)
}
}
lastDoc = null
}
override fun onBlock(node: MiniBlock) { override fun onBlock(node: MiniBlock) {
blocks.addLast(node) blocks.addLast(node)
} }

View File

@ -0,0 +1,467 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.tools
import net.sergeych.lyng.*
import net.sergeych.lyng.binding.Binder
import net.sergeych.lyng.binding.BindingSnapshot
import net.sergeych.lyng.binding.SymbolKind
import net.sergeych.lyng.bytecode.BytecodeStatement
import net.sergeych.lyng.bytecode.CmdDisassembler
import net.sergeych.lyng.format.LyngFormatConfig
import net.sergeych.lyng.format.LyngFormatter
import net.sergeych.lyng.highlight.HighlightSpan
import net.sergeych.lyng.highlight.SimpleLyngHighlighter
import net.sergeych.lyng.highlight.TextRange
import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.pacman.ImportProvider
import net.sergeych.lyng.resolution.ResolutionCollector
import net.sergeych.lyng.resolution.ResolutionReport
data class LyngAnalysisRequest(
val text: String,
val fileName: String = "<snippet>",
val importProvider: ImportProvider = Script.defaultImportManager,
val seedScope: Scope? = null
)
enum class LyngDiagnosticSeverity { Error, Warning }
data class LyngDiagnostic(
val message: String,
val severity: LyngDiagnosticSeverity,
val range: TextRange? = null,
val pos: Pos? = null
)
data class LyngAnalysisResult(
val source: Source,
val text: String,
val mini: MiniScript?,
val binding: BindingSnapshot?,
val resolution: ResolutionReport?,
val importedModules: List<String>,
val diagnostics: List<LyngDiagnostic>,
val lexicalHighlights: List<HighlightSpan>
)
data class LyngSymbolTarget(
val name: String,
val kind: SymbolKind,
val range: TextRange,
val containerName: String? = null
)
data class LyngSymbolInfo(
val target: LyngSymbolTarget,
val signature: String? = null,
val doc: MiniDoc? = null
)
enum class LyngSemanticKind {
Function,
Class,
Enum,
TypeAlias,
Value,
Variable,
Parameter,
TypeRef,
EnumConstant
}
data class LyngSemanticSpan(
val range: TextRange,
val kind: LyngSemanticKind
)
object LyngLanguageTools {
suspend fun analyze(request: LyngAnalysisRequest): LyngAnalysisResult {
val source = Source(request.fileName, request.text)
val miniSink = MiniAstBuilder()
val resolutionCollector = ResolutionCollector(source.fileName)
val diagnostics = ArrayList<LyngDiagnostic>()
try {
Compiler.compileWithResolution(
source,
request.importProvider,
miniSink = miniSink,
resolutionSink = resolutionCollector,
useBytecodeStatements = false,
allowUnresolvedRefs = true,
seedScope = request.seedScope
)
} catch (t: Throwable) {
val pos = (t as? net.sergeych.lyng.ScriptError)?.pos
diagnostics += LyngDiagnostic(
message = t.message ?: t.toString(),
severity = LyngDiagnosticSeverity.Error,
range = pos?.let { posToRange(source, it) },
pos = pos
)
}
val mini = miniSink.build()
val binding = mini?.let { Binder.bind(request.text, it) }
val report = try { resolutionCollector.buildReport() } catch (_: Throwable) { null }
report?.errors?.forEach { err ->
diagnostics += LyngDiagnostic(
message = err.message,
severity = LyngDiagnosticSeverity.Error,
range = posToRange(source, err.pos),
pos = err.pos
)
}
report?.warnings?.forEach { warn ->
diagnostics += LyngDiagnostic(
message = warn.message,
severity = LyngDiagnosticSeverity.Warning,
range = posToRange(source, warn.pos),
pos = warn.pos
)
}
val imports = when {
mini != null -> DocLookupUtils.canonicalImportedModules(mini, request.text)
else -> DocLookupUtils.extractImportsFromText(request.text).toMutableList().apply { add("lyng.stdlib") }.distinct()
}
val lexical = try { SimpleLyngHighlighter().highlight(request.text) } catch (_: Throwable) { emptyList() }
return LyngAnalysisResult(
source = source,
text = request.text,
mini = mini,
binding = binding,
resolution = report,
importedModules = imports,
diagnostics = diagnostics,
lexicalHighlights = lexical
)
}
suspend fun analyze(text: String, fileName: String = "<snippet>"): LyngAnalysisResult =
analyze(LyngAnalysisRequest(text = text, fileName = fileName))
fun format(text: String, config: LyngFormatConfig = LyngFormatConfig()): String =
LyngFormatter.format(text, config)
fun lexicalHighlights(text: String): List<HighlightSpan> =
SimpleLyngHighlighter().highlight(text)
fun semanticHighlights(analysis: LyngAnalysisResult): List<LyngSemanticSpan> {
val mini = analysis.mini ?: return emptyList()
val source = analysis.source
val out = ArrayList<LyngSemanticSpan>(128)
val covered = HashSet<Pair<Int, Int>>()
fun addRange(start: Int, end: Int, kind: LyngSemanticKind) {
if (start < 0 || end <= start || end > analysis.text.length) return
val key = start to end
if (covered.add(key)) out += LyngSemanticSpan(TextRange(start, end), kind)
}
fun addName(pos: Pos, name: String, kind: LyngSemanticKind) {
val s = source.offsetOf(pos)
addRange(s, s + name.length, kind)
}
fun addTypeSegments(t: MiniTypeRef?) {
when (t) {
is MiniTypeName -> t.segments.forEach { seg ->
addName(seg.range.start, seg.name, LyngSemanticKind.TypeRef)
}
is MiniGenericType -> {
addTypeSegments(t.base)
t.args.forEach { addTypeSegments(it) }
}
is MiniFunctionType -> {
t.receiver?.let { addTypeSegments(it) }
t.params.forEach { addTypeSegments(it) }
addTypeSegments(t.returnType)
}
is MiniTypeVar -> {
addRange(source.offsetOf(t.range.start), source.offsetOf(t.range.end), LyngSemanticKind.TypeRef)
}
is MiniTypeUnion -> {
t.options.forEach { addTypeSegments(it) }
}
is MiniTypeIntersection -> {
t.options.forEach { addTypeSegments(it) }
}
null -> {}
}
}
fun addDeclTypeSegments(d: MiniDecl) {
when (d) {
is MiniFunDecl -> {
addTypeSegments(d.returnType)
d.params.forEach { addTypeSegments(it.type) }
addTypeSegments(d.receiver)
}
is MiniValDecl -> {
addTypeSegments(d.type)
addTypeSegments(d.receiver)
}
is MiniClassDecl -> {
d.ctorFields.forEach { addTypeSegments(it.type) }
d.classFields.forEach { addTypeSegments(it.type) }
d.members.forEach { m ->
when (m) {
is MiniMemberFunDecl -> {
addTypeSegments(m.returnType)
m.params.forEach { addTypeSegments(it.type) }
}
is MiniMemberValDecl -> addTypeSegments(m.type)
is MiniMemberTypeAliasDecl -> addTypeSegments(m.target)
is MiniInitDecl -> {}
}
}
}
is MiniEnumDecl -> {}
is MiniTypeAliasDecl -> addTypeSegments(d.target)
}
}
for (d in mini.declarations) {
when (d) {
is MiniFunDecl -> addName(d.nameStart, d.name, LyngSemanticKind.Function)
is MiniClassDecl -> addName(d.nameStart, d.name, LyngSemanticKind.Class)
is MiniEnumDecl -> addName(d.nameStart, d.name, LyngSemanticKind.Enum)
is MiniValDecl -> addName(d.nameStart, d.name, if (d.mutable) LyngSemanticKind.Variable else LyngSemanticKind.Value)
is MiniTypeAliasDecl -> addName(d.nameStart, d.name, LyngSemanticKind.TypeAlias)
}
addDeclTypeSegments(d)
}
mini.imports.forEach { imp ->
imp.segments.forEach { seg ->
addRange(source.offsetOf(seg.range.start), source.offsetOf(seg.range.end), LyngSemanticKind.TypeRef)
}
}
fun addParams(params: List<MiniParam>) {
params.forEach { p -> addName(p.nameStart, p.name, LyngSemanticKind.Parameter) }
}
mini.declarations.forEach { d ->
when (d) {
is MiniFunDecl -> addParams(d.params)
is MiniClassDecl -> d.members.filterIsInstance<MiniMemberFunDecl>().forEach { addParams(it.params) }
else -> {}
}
}
mini.declarations.filterIsInstance<MiniEnumDecl>().forEach { en ->
en.entries.zip(en.entryPositions).forEach { (name, pos) ->
addName(pos, name, LyngSemanticKind.EnumConstant)
}
}
analysis.binding?.let { binding ->
val declKeys = binding.symbols.map { it.declStart to it.declEnd }.toSet()
for (ref in binding.references) {
if (declKeys.contains(ref.start to ref.end)) continue
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId } ?: continue
val kind = when (sym.kind) {
SymbolKind.Function -> LyngSemanticKind.Function
SymbolKind.Class -> LyngSemanticKind.Class
SymbolKind.Enum -> LyngSemanticKind.Enum
SymbolKind.TypeAlias -> LyngSemanticKind.TypeAlias
SymbolKind.Value -> LyngSemanticKind.Value
SymbolKind.Variable -> LyngSemanticKind.Variable
SymbolKind.Parameter -> LyngSemanticKind.Parameter
}
addRange(ref.start, ref.end, kind)
}
}
return out.sortedBy { it.range.start }
}
suspend fun completions(text: String, offset: Int, analysis: LyngAnalysisResult? = null): List<CompletionItem> {
val mini = analysis?.mini
val binding = analysis?.binding
StdlibDocsBootstrap.ensure()
return CompletionEngineLight.completeSuspend(text, offset, mini, binding)
}
fun definitionAt(analysis: LyngAnalysisResult, offset: Int): LyngSymbolTarget? {
val binding = analysis.binding ?: return null
val sym = binding.symbols.firstOrNull { offset in it.declStart until it.declEnd }
?: binding.references.firstOrNull { offset in it.start until it.end }
?.let { ref -> binding.symbols.firstOrNull { it.id == ref.symbolId } }
?: return null
val containerName = sym.containerId?.let { cid -> binding.symbols.firstOrNull { it.id == cid }?.name }
return LyngSymbolTarget(
name = sym.name,
kind = sym.kind,
range = TextRange(sym.declStart, sym.declEnd),
containerName = containerName
)
}
fun usagesAt(analysis: LyngAnalysisResult, offset: Int, includeDeclaration: Boolean = false): List<TextRange> {
val binding = analysis.binding ?: return emptyList()
val sym = binding.symbols.firstOrNull { offset in it.declStart until it.declEnd }
?: binding.references.firstOrNull { offset in it.start until it.end }
?.let { ref -> binding.symbols.firstOrNull { it.id == ref.symbolId } }
?: return emptyList()
val ranges = binding.references.filter { it.symbolId == sym.id }.map { TextRange(it.start, it.end) }.toMutableList()
if (includeDeclaration) ranges.add(TextRange(sym.declStart, sym.declEnd))
return ranges
}
fun docAt(analysis: LyngAnalysisResult, offset: Int): LyngSymbolInfo? {
StdlibDocsBootstrap.ensure()
val target = definitionAt(analysis, offset) ?: return null
val mini = analysis.mini
val imported = analysis.importedModules
val name = target.name
val local = mini?.let { findLocalDecl(it, analysis.text, name, target.range.start) }
if (local != null) {
val signature = signatureOf(local.first, local.second)
return LyngSymbolInfo(target, signature = signature, doc = local.first.doc)
}
if (target.containerName != null) {
val member = DocLookupUtils.resolveMemberWithInheritance(imported, target.containerName, name, mini)
if (member != null) {
val signature = signatureOf(member.second, member.first)
return LyngSymbolInfo(target, signature = signature, doc = member.second.doc)
}
}
for (mod in imported) {
val decl = BuiltinDocRegistry.docsForModule(mod).firstOrNull { it.name == name }
if (decl != null) {
val signature = signatureOf(decl, null)
return LyngSymbolInfo(target, signature = signature, doc = decl.doc)
}
}
return LyngSymbolInfo(target, signature = null, doc = null)
}
suspend fun disassembleSymbol(
code: String,
symbol: String,
importProvider: ImportProvider = Script.defaultImportManager
): String {
val script = Compiler.compile(code.toSource(), importProvider)
val scope = importProvider.newStdScope(Pos.builtIn)
script.execute(scope)
val (container, member) = splitMember(symbol)
if (member == null) return disassembleFromScope(scope, container)
val rec = scope.get(container) ?: return "$symbol is not found"
val cls = rec.value as? ObjClass ?: return "$container is not a class"
val classScope = cls.classScope ?: return "$container has no class scope"
return disassembleFromScope(classScope, member)
}
private fun posToRange(source: Source, pos: Pos): TextRange {
val s = source.offsetOf(pos)
return TextRange(s, (s + 1).coerceAtMost(source.text.length))
}
private fun findLocalDecl(mini: MiniScript, text: String, name: String, declStart: Int): Pair<MiniNamedDecl, String?>? {
val src = mini.range.start.source
fun matches(p: Pos, len: Int) = src.offsetOf(p).let { s -> s == declStart && len > 0 }
for (d in mini.declarations) {
if (d.name == name && matches(d.nameStart, d.name.length)) return d to null
if (d is MiniClassDecl) {
d.members.forEach { m ->
if (m.name == name && matches(m.nameStart, m.name.length)) return m to d.name
}
d.ctorFields.firstOrNull { it.name == name && matches(it.nameStart, it.name.length) }?.let {
return DocLookupUtils.toMemberVal(it) to d.name
}
d.classFields.firstOrNull { it.name == name && matches(it.nameStart, it.name.length) }?.let {
return DocLookupUtils.toMemberVal(it) to d.name
}
}
}
return null
}
private fun signatureOf(decl: MiniNamedDecl, ownerClass: String?): String? {
val owner = ownerClass?.let { "$it." } ?: ""
return when (decl) {
is MiniFunDecl -> {
val params = decl.params.joinToString(", ") { it.name + typeSuffix(it.type) }
val ret = typeSuffix(decl.returnType)
"fun $owner${decl.name}($params)$ret"
}
is MiniMemberFunDecl -> {
val params = decl.params.joinToString(", ") { it.name + typeSuffix(it.type) }
val ret = typeSuffix(decl.returnType)
"fun $owner${decl.name}($params)$ret"
}
is MiniValDecl -> {
val kw = if (decl.mutable) "var" else "val"
"$kw $owner${decl.name}${typeSuffix(decl.type)}"
}
is MiniMemberValDecl -> {
val kw = if (decl.mutable) "var" else "val"
"$kw $owner${decl.name}${typeSuffix(decl.type)}"
}
is MiniClassDecl -> {
val bases = if (decl.bases.isEmpty()) "" else ": " + decl.bases.joinToString(", ")
"class ${decl.name}$bases"
}
is MiniEnumDecl -> "enum ${decl.name}"
is MiniTypeAliasDecl -> {
val tp = if (decl.typeParams.isEmpty()) "" else "<" + decl.typeParams.joinToString(", ") + ">"
"type ${decl.name}$tp = ${DocLookupUtils.typeOf(decl.target)}"
}
is MiniMemberTypeAliasDecl -> {
val tp = if (decl.typeParams.isEmpty()) "" else "<" + decl.typeParams.joinToString(", ") + ">"
"type $owner${decl.name}$tp = ${DocLookupUtils.typeOf(decl.target)}"
}
else -> null
}
}
private fun typeSuffix(type: MiniTypeRef?): String =
type?.let { ": ${DocLookupUtils.typeOf(it)}" } ?: ""
private fun splitMember(symbol: String): Pair<String, String?> {
val idx = symbol.lastIndexOf('.')
return if (idx >= 0 && idx + 1 < symbol.length) {
symbol.substring(0, idx) to symbol.substring(idx + 1)
} else {
symbol to null
}
}
private fun disassembleFromScope(scope: Scope, name: String): String {
val record = scope.get(name) ?: return "$name is not found"
val stmt = record.value as? net.sergeych.lyng.Statement ?: return "$name is not a compiled body"
val bytecode = (stmt as? BytecodeStatement)?.bytecodeFunction()
?: (stmt as? BytecodeBodyProvider)?.bytecodeBody()?.bytecodeFunction()
?: return "$name is not a compiled body"
return CmdDisassembler.disassemble(bytecode)
}
}

View File

@ -131,4 +131,16 @@ class StdlibTest {
assertEquals(31, p.age) assertEquals(31, p.age)
""".trimIndent()) """.trimIndent())
} }
// @Test
// fun testFunFromSample() = runTest {
// range should be iterable if it is intrange
// eval("""
// val data = 1..5 // or [1, 2, 3, 4, 5]
// fun test() {
// data.filter { it % 2 == 0 }.map { it * it }
// }
// test()
// """.trimIndent())
// }
} }

View File

@ -0,0 +1,113 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.tools
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.miniast.MiniClassDecl
import net.sergeych.lyng.miniast.MiniMemberTypeAliasDecl
import net.sergeych.lyng.stdlib_included.rootLyng
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class LyngLanguageToolsTest {
@Test
fun languageTools_dryRun_rootLyng_hasNoErrors() = runTest {
val res = LyngLanguageTools.analyze(rootLyng, "root.lyng")
assertNotNull(res.mini, "root.lyng should build Mini-AST")
assertTrue(res.lexicalHighlights.isNotEmpty(), "root.lyng should produce lexical highlights")
}
@Test
fun languageTools_tracks_inner_and_type_aliases() = runTest {
val code = """
/** Box docs */
type Box<T> = List<T?>
class Outer {
type Alias = Box<Int>
class Inner {
val value: Int = 1
}
enum Kind { A, B }
object Obj { val flag = true }
}
""".trimIndent()
val res = LyngLanguageTools.analyze(code, "inner.lyng")
val mini = res.mini
assertNotNull(mini, "Mini-AST must be built")
val outer = mini.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "Outer" }
assertNotNull(outer, "Outer class should be captured")
val aliasMember = outer.members.filterIsInstance<MiniMemberTypeAliasDecl>().firstOrNull { it.name == "Alias" }
assertNotNull(aliasMember, "Inner type alias should be captured as a class member")
val sem = LyngLanguageTools.semanticHighlights(res)
assertTrue(sem.any { it.kind == LyngSemanticKind.TypeAlias }, "Type aliases should be part of semantic highlights")
}
@Test
fun languageTools_completion_and_docs_for_type_alias() = runTest {
val code = """
/** Box docs */
type Box<T> = List<T>
val x: Box<Int> = [1]
<caret>
""".trimIndent()
val caret = code.indexOf("<caret>")
val text = code.replace("<caret>", "")
val res = LyngLanguageTools.analyze(text, "alias.lyng")
val items = LyngLanguageTools.completions(text, caret, res)
assertTrue(items.any { it.name == "Box" }, "Completion should include Box type alias")
val aliasOffset = text.indexOf("Box<Int>")
val doc = LyngLanguageTools.docAt(res, aliasOffset)
assertNotNull(doc, "Docs should resolve for Box")
assertEquals("Box", doc.target.name)
assertEquals("Box docs", doc.doc?.summary)
}
@Test
fun languageTools_definition_and_usages() = runTest {
val code = """
val answer = 42
println(answer)
answer
""".trimIndent()
val res = LyngLanguageTools.analyze(code, "usage.lyng")
val usageOffset = code.lastIndexOf("answer")
val def = LyngLanguageTools.definitionAt(res, usageOffset)
assertNotNull(def, "Definition should resolve")
assertEquals("answer", def.name)
val usages = LyngLanguageTools.usagesAt(res, usageOffset)
assertTrue(usages.size >= 2, "Expected at least two usages, got ${usages.size}")
}
@Test
fun languageTools_disassemble_symbol() = runTest {
val code = """
fun add(a: Int, b: Int): Int {
a + b
}
""".trimIndent()
val dis = LyngLanguageTools.disassembleSymbol(code, "add")
assertTrue(!dis.contains("not a compiled body"), "Disassembly should be produced, got: $dis")
}
}

View File

@ -19,9 +19,17 @@ package net.sergeych.lyngweb
import androidx.compose.runtime.* import androidx.compose.runtime.*
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.coroutines.launch
import net.sergeych.lyng.highlight.TextRange
import net.sergeych.lyng.miniast.CompletionItem
import net.sergeych.lyng.tools.LyngAnalysisResult
import net.sergeych.lyng.tools.LyngSymbolInfo
import net.sergeych.lyng.tools.LyngSymbolTarget
import org.jetbrains.compose.web.attributes.placeholder import org.jetbrains.compose.web.attributes.placeholder
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.events.SyntheticKeyboardEvent import org.jetbrains.compose.web.events.SyntheticKeyboardEvent
import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLTextAreaElement import org.w3c.dom.HTMLTextAreaElement
@ -47,12 +55,20 @@ fun EditorWithOverlay(
setCode: (String) -> Unit, setCode: (String) -> Unit,
tabSize: Int = 4, tabSize: Int = 4,
onKeyDown: ((SyntheticKeyboardEvent) -> Unit)? = null, onKeyDown: ((SyntheticKeyboardEvent) -> Unit)? = null,
onAnalysisReady: ((LyngAnalysisResult) -> Unit)? = null,
onCompletionRequested: ((Int, List<CompletionItem>) -> Unit)? = null,
onDefinitionResolved: ((Int, LyngSymbolTarget?) -> Unit)? = null,
onUsagesResolved: ((Int, List<TextRange>) -> Unit)? = null,
onDocRequested: ((Int, LyngSymbolInfo?) -> Unit)? = null,
// New sizing controls // New sizing controls
minRows: Int = 6, minRows: Int = 6,
maxRows: Int? = null, maxRows: Int? = null,
autoGrow: Boolean = false, autoGrow: Boolean = false,
) { ) {
val scope = rememberCoroutineScope()
var containerEl by remember { mutableStateOf<HTMLElement?>(null) }
var overlayEl by remember { mutableStateOf<HTMLElement?>(null) } var overlayEl by remember { mutableStateOf<HTMLElement?>(null) }
var diagOverlayEl by remember { mutableStateOf<HTMLElement?>(null) }
var taEl by remember { mutableStateOf<HTMLTextAreaElement?>(null) } var taEl by remember { mutableStateOf<HTMLTextAreaElement?>(null) }
var lastGoodHtml by remember { mutableStateOf<String?>(null) } var lastGoodHtml by remember { mutableStateOf<String?>(null) }
var lastGoodText by remember { mutableStateOf<String?>(null) } var lastGoodText by remember { mutableStateOf<String?>(null) }
@ -62,9 +78,16 @@ fun EditorWithOverlay(
var pendingScrollLeft by remember { mutableStateOf<Double?>(null) } var pendingScrollLeft by remember { mutableStateOf<Double?>(null) }
var cachedLineHeight by remember { mutableStateOf<Double?>(null) } var cachedLineHeight by remember { mutableStateOf<Double?>(null) }
var cachedVInsets by remember { mutableStateOf<Double?>(null) } var cachedVInsets by remember { mutableStateOf<Double?>(null) }
var cachedCharWidth by remember { mutableStateOf<Double?>(null) }
var lastAnalysis by remember { mutableStateOf<LyngAnalysisResult?>(null) }
var lastAnalysisText by remember { mutableStateOf<String?>(null) }
var lineStarts by remember { mutableStateOf(IntArray(0)) }
var tooltipText by remember { mutableStateOf<String?>(null) }
var tooltipX by remember { mutableStateOf<Double?>(null) }
var tooltipY by remember { mutableStateOf<Double?>(null) }
fun ensureMetrics(ta: HTMLTextAreaElement) { fun ensureMetrics(ta: HTMLTextAreaElement) {
if (cachedLineHeight == null || cachedVInsets == null) { if (cachedLineHeight == null || cachedVInsets == null || cachedCharWidth == null) {
val cs = window.getComputedStyle(ta) val cs = window.getComputedStyle(ta)
val lhStr = cs.getPropertyValue("line-height").trim() val lhStr = cs.getPropertyValue("line-height").trim()
val lh = lhStr.removeSuffix("px").toDoubleOrNull() ?: 20.0 val lh = lhStr.removeSuffix("px").toDoubleOrNull() ?: 20.0
@ -78,6 +101,21 @@ fun EditorWithOverlay(
val bb = parsePx("border-bottom-width") val bb = parsePx("border-bottom-width")
cachedLineHeight = lh cachedLineHeight = lh
cachedVInsets = pt + pb + bt + bb cachedVInsets = pt + pb + bt + bb
val canvas = window.document.createElement("canvas") as HTMLCanvasElement
val ctx = canvas.getContext("2d") as? CanvasRenderingContext2D
if (ctx != null) {
val fontSize = cs.fontSize
val fontFamily = cs.fontFamily
val fontWeight = cs.fontWeight
val fontStyle = cs.fontStyle
ctx.font = "$fontStyle $fontWeight $fontSize $fontFamily"
val m = ctx.measureText("M")
val w = if (m.width > 0.0) m.width else 8.0
cachedCharWidth = w
} else {
cachedCharWidth = 8.0
}
} }
} }
@ -102,6 +140,63 @@ fun EditorWithOverlay(
ta.style.height = "${target}px" ta.style.height = "${target}px"
} }
suspend fun ensureAnalysis(text: String): LyngAnalysisResult {
val cached = lastAnalysis
val cachedText = lastAnalysisText
if (cached != null && cachedText == text) return cached
val analysis = LyngWebTools.analyze(text)
lastAnalysis = analysis
lastAnalysisText = text
onAnalysisReady?.invoke(analysis)
return analysis
}
fun htmlEscapeLocal(s: String): String = buildString(s.length) {
for (ch in s) when (ch) {
'<' -> append("&lt;")
'>' -> append("&gt;")
'&' -> append("&amp;")
'"' -> append("&quot;")
'\'' -> append("&#39;")
else -> append(ch)
}
}
fun buildLineStarts(text: String): IntArray {
val starts = ArrayList<Int>(maxOf(1, text.length / 16))
starts.add(0)
for (i in text.indices) {
if (text[i] == '\n') starts.add(i + 1)
}
return starts.toIntArray()
}
fun offsetFromMouse(ta: HTMLTextAreaElement, clientX: Double, clientY: Double): Int? {
ensureMetrics(ta)
val rect = ta.getBoundingClientRect()
val lineHeight = cachedLineHeight ?: return null
val charWidth = cachedCharWidth ?: return null
val cs = window.getComputedStyle(ta)
fun parsePx(name: String): Double {
val v = cs.getPropertyValue(name).trim().removeSuffix("px").toDoubleOrNull()
return v ?: 0.0
}
val padLeft = parsePx("padding-left") + parsePx("border-left-width")
val padTop = parsePx("padding-top") + parsePx("border-top-width")
val x = clientX - rect.left + ta.scrollLeft - padLeft
val y = clientY - rect.top + ta.scrollTop - padTop
if (y < 0) return 0
val lineIdx = (y / lineHeight).toInt().coerceAtLeast(0)
if (lineStarts.isEmpty()) return 0
val actualLineIdx = lineIdx.coerceAtMost(lineStarts.size - 1)
val lineStart = lineStarts[actualLineIdx]
val lineEnd = if (actualLineIdx + 1 < lineStarts.size) lineStarts[actualLineIdx + 1] - 1 else code.length
val lineLen = (lineEnd - lineStart).coerceAtLeast(0)
val col = (x / charWidth).toInt().coerceAtLeast(0)
val clampedCol = col.coerceAtMost(lineLen)
return (lineStart + clampedCol).coerceIn(0, code.length)
}
// Update overlay HTML whenever code changes // Update overlay HTML whenever code changes
LaunchedEffect(code) { LaunchedEffect(code) {
fun clamp(i: Int, lo: Int, hi: Int): Int = if (i < lo) lo else if (i > hi) hi else i fun clamp(i: Int, lo: Int, hi: Int): Int = if (i < lo) lo else if (i > hi) hi else i
@ -110,16 +205,6 @@ fun EditorWithOverlay(
val e = clamp(end, 0, text.length) val e = clamp(end, 0, text.length)
return if (e <= s) "" else text.substring(s, e) return if (e <= s) "" else text.substring(s, e)
} }
fun htmlEscapeLocal(s: String): String = buildString(s.length) {
for (ch in s) when (ch) {
'<' -> append("&lt;")
'>' -> append("&gt;")
'&' -> append("&amp;")
'"' -> append("&quot;")
'\'' -> append("&#39;")
else -> append(ch)
}
}
fun trimHtmlToTextPrefix(html: String, prefixChars: Int): String { fun trimHtmlToTextPrefix(html: String, prefixChars: Int): String {
if (prefixChars <= 0) return "" if (prefixChars <= 0) return ""
@ -188,10 +273,103 @@ fun EditorWithOverlay(
val sl = pendingScrollLeft ?: (taEl?.scrollLeft ?: 0.0) val sl = pendingScrollLeft ?: (taEl?.scrollLeft ?: 0.0)
overlayEl?.scrollTop = st overlayEl?.scrollTop = st
overlayEl?.scrollLeft = sl overlayEl?.scrollLeft = sl
diagOverlayEl?.scrollTop = st
diagOverlayEl?.scrollLeft = sl
pendingScrollTop = null pendingScrollTop = null
pendingScrollLeft = null pendingScrollLeft = null
// If text changed and autoGrow enabled, adjust height // If text changed and autoGrow enabled, adjust height
adjustTextareaHeight() adjustTextareaHeight()
lineStarts = buildLineStarts(code)
}
fun buildDiagnosticsHtml(
text: String,
diagnostics: List<net.sergeych.lyng.tools.LyngDiagnostic>
): String {
if (diagnostics.isEmpty()) return ""
val ranges = diagnostics.mapNotNull { d ->
val r = d.range ?: return@mapNotNull null
if (r.start < 0 || r.endExclusive <= r.start || r.endExclusive > text.length) return@mapNotNull null
Triple(r, d.severity, d.message)
}.sortedBy { it.first.start }
if (ranges.isEmpty()) return ""
val out = StringBuilder(text.length + 64)
var cursor = 0
for ((range, severity, message) in ranges) {
if (range.start < cursor) continue
if (cursor < range.start) {
out.append(htmlEscapeLocal(text.substring(cursor, range.start)))
}
val color = when (severity) {
net.sergeych.lyng.tools.LyngDiagnosticSeverity.Error -> "#dc3545"
net.sergeych.lyng.tools.LyngDiagnosticSeverity.Warning -> "#ffc107"
}
val seg = htmlEscapeLocal(text.substring(range.start, range.endExclusive))
val tip = htmlEscapeLocal(message).replace("\"", "&quot;")
out.append("<span title=\"").append(tip).append("\" style=\"text-decoration-line:underline;text-decoration-style:wavy;")
out.append("text-decoration-color:").append(color).append(";\">")
out.append(seg)
out.append("</span>")
cursor = range.endExclusive
}
if (cursor < text.length) out.append(htmlEscapeLocal(text.substring(cursor)))
return out.toString()
}
fun diagnosticMessageAt(offset: Int, analysis: LyngAnalysisResult?): String? {
val list = analysis?.diagnostics ?: return null
for (d in list) {
val r = d.range ?: continue
if (offset in r.start until r.endExclusive) return d.message
}
return null
}
fun updateCaretTooltip() {
val ta = taEl ?: return
val offset = ta.selectionStart ?: return
val msg = diagnosticMessageAt(offset, lastAnalysis)
if (msg.isNullOrBlank()) {
ta.removeAttribute("title")
} else {
ta.setAttribute("title", msg)
}
}
fun updateHoverTooltip(clientX: Double, clientY: Double) {
val ta = taEl ?: return
val offset = offsetFromMouse(ta, clientX, clientY) ?: return
val msg = diagnosticMessageAt(offset, lastAnalysis)
if (msg.isNullOrBlank()) {
tooltipText = null
return
}
val container = containerEl ?: return
val rect = container.getBoundingClientRect()
tooltipText = msg
tooltipX = (clientX - rect.left + 12.0).coerceAtLeast(0.0)
tooltipY = (clientY - rect.top + 12.0).coerceAtLeast(0.0)
}
LaunchedEffect(code, lastAnalysis) {
val analysis = lastAnalysis ?: return@LaunchedEffect
if (lastAnalysisText != code) {
diagOverlayEl?.innerHTML = htmlEscapeLocal(code)
updateCaretTooltip()
return@LaunchedEffect
}
val html = buildDiagnosticsHtml(code, analysis.diagnostics)
val content = if (html.isEmpty()) htmlEscapeLocal(code) else html
diagOverlayEl?.innerHTML = content
updateCaretTooltip()
}
LaunchedEffect(code, onAnalysisReady) {
if (onAnalysisReady == null) return@LaunchedEffect
try {
ensureAnalysis(code)
} catch (_: Throwable) {
}
} }
fun setSelection(start: Int, end: Int = start) { fun setSelection(start: Int, end: Int = start) {
@ -206,6 +384,10 @@ fun EditorWithOverlay(
// avoid external CSS dependency: ensure base positioning inline // avoid external CSS dependency: ensure base positioning inline
classes("position-relative") classes("position-relative")
attr("style", "position:relative;") attr("style", "position:relative;")
ref { it ->
containerEl = it
onDispose { if (containerEl === it) containerEl = null }
}
}) { }) {
// Overlay: highlighted code // Overlay: highlighted code
org.jetbrains.compose.web.dom.Div({ org.jetbrains.compose.web.dom.Div({
@ -226,6 +408,23 @@ fun EditorWithOverlay(
} }
}) {} }) {}
// Diagnostics overlay: transparent text with wavy underlines
org.jetbrains.compose.web.dom.Div({
attr(
"style",
buildString {
append("position:absolute; left:0; top:0; right:0; bottom:0;")
append("overflow:auto; box-sizing:border-box; white-space:pre-wrap; word-break:break-word; tab-size:")
append(tabSize)
append("; margin:0; pointer-events:none; color:transparent;")
}
)
ref { it ->
diagOverlayEl = it
onDispose { if (diagOverlayEl === it) diagOverlayEl = null }
}
}) {}
// Textarea: user input with transparent text // Textarea: user input with transparent text
org.jetbrains.compose.web.dom.TextArea(value = code, attrs = { org.jetbrains.compose.web.dom.TextArea(value = code, attrs = {
ref { ta -> ref { ta ->
@ -269,13 +468,16 @@ fun EditorWithOverlay(
val v = (ev.target as HTMLTextAreaElement).value val v = (ev.target as HTMLTextAreaElement).value
setCode(v) setCode(v)
adjustTextareaHeight() adjustTextareaHeight()
updateCaretTooltip()
} }
onKeyDown { ev -> onKeyDown { ev ->
// bubble to caller first so they may intercept shortcuts // bubble to caller first so they may intercept shortcuts
onKeyDown?.invoke(ev) onKeyDown?.invoke(ev)
if (ev.defaultPrevented) return@onKeyDown
val ta = taEl ?: return@onKeyDown val ta = taEl ?: return@onKeyDown
val key = ev.key val key = ev.key
val keyLower = key.lowercase()
// If user pressed Ctrl/Cmd + Enter, treat it as a shortcut (e.g., Run) // If user pressed Ctrl/Cmd + Enter, treat it as a shortcut (e.g., Run)
// and DO NOT insert a newline here. Let the host handler act. // and DO NOT insert a newline here. Let the host handler act.
// Also prevent default so the textarea won't add a line. // Also prevent default so the textarea won't add a line.
@ -283,6 +485,48 @@ fun EditorWithOverlay(
ev.preventDefault() ev.preventDefault()
return@onKeyDown return@onKeyDown
} }
if (ev.ctrlKey || ev.metaKey) {
val offset = ta.selectionStart ?: 0
val text = ta.value
when {
(key == " " || keyLower == "space" || keyLower == "spacebar") && onCompletionRequested != null -> {
ev.preventDefault()
scope.launch {
val analysis = ensureAnalysis(text)
val items = LyngWebTools.completions(text, offset, analysis)
onCompletionRequested(offset, items)
}
return@onKeyDown
}
keyLower == "b" && onDefinitionResolved != null -> {
ev.preventDefault()
scope.launch {
val analysis = ensureAnalysis(text)
val target = LyngWebTools.definitionAt(analysis, offset)
onDefinitionResolved(offset, target)
}
return@onKeyDown
}
ev.shiftKey && keyLower == "u" && onUsagesResolved != null -> {
ev.preventDefault()
scope.launch {
val analysis = ensureAnalysis(text)
val ranges = LyngWebTools.usagesAt(analysis, offset, includeDeclaration = false)
onUsagesResolved(offset, ranges)
}
return@onKeyDown
}
keyLower == "q" && onDocRequested != null -> {
ev.preventDefault()
scope.launch {
val analysis = ensureAnalysis(text)
val info = LyngWebTools.docAt(analysis, offset)
onDocRequested(offset, info)
}
return@onKeyDown
}
}
}
if (key == "Tab" && ev.shiftKey) { if (key == "Tab" && ev.shiftKey) {
// Shift+Tab: outdent current line(s) // Shift+Tab: outdent current line(s)
ev.preventDefault() ev.preventDefault()
@ -336,21 +580,57 @@ fun EditorWithOverlay(
} }
} }
onKeyUp { _ ->
updateCaretTooltip()
}
onMouseUp { _ ->
updateCaretTooltip()
}
onMouseMove { ev ->
updateHoverTooltip(ev.clientX.toDouble(), ev.clientY.toDouble())
}
onMouseLeave { _ ->
tooltipText = null
}
onScroll { ev -> onScroll { ev ->
val src = ev.target as? HTMLTextAreaElement ?: return@onScroll val src = ev.target as? HTMLTextAreaElement ?: return@onScroll
overlayEl?.scrollTop = src.scrollTop overlayEl?.scrollTop = src.scrollTop
overlayEl?.scrollLeft = src.scrollLeft overlayEl?.scrollLeft = src.scrollLeft
diagOverlayEl?.scrollTop = src.scrollTop
diagOverlayEl?.scrollLeft = src.scrollLeft
} }
}) })
if (tooltipText != null && tooltipX != null && tooltipY != null) {
org.jetbrains.compose.web.dom.Div({
attr(
"style",
buildString {
append("position:absolute; z-index:3; pointer-events:none;")
append("left:").append(tooltipX).append("px; top:").append(tooltipY).append("px;")
append("background:#212529; color:#f8f9fa; padding:4px 6px; border-radius:4px;")
append("font-size:12px; line-height:1.3; max-width:360px; white-space:pre-wrap;")
append("box-shadow:0 4px 10px rgba(0,0,0,.15);")
}
)
}) {
org.jetbrains.compose.web.dom.Text(tooltipText!!)
}
}
// No built-in action buttons: EditorWithOverlay is a pure editor now // No built-in action buttons: EditorWithOverlay is a pure editor now
} }
// Ensure overlay typography and paddings mirror the textarea so characters line up 1:1 // Ensure overlay typography and paddings mirror the textarea so characters line up 1:1
LaunchedEffect(taEl, overlayEl) { LaunchedEffect(taEl, overlayEl, diagOverlayEl) {
try { try {
val ta = taEl ?: return@LaunchedEffect val ta = taEl ?: return@LaunchedEffect
val ov = overlayEl ?: return@LaunchedEffect val ov = overlayEl ?: return@LaunchedEffect
val diag = diagOverlayEl
val cs = window.getComputedStyle(ta) val cs = window.getComputedStyle(ta)
// Best-effort concrete line-height // Best-effort concrete line-height
@ -376,6 +656,7 @@ fun EditorWithOverlay(
append("color: var(--bs-body-color);") append("color: var(--bs-body-color);")
} }
ov.setAttribute("style", style) ov.setAttribute("style", style)
diag?.setAttribute("style", style + "color:transparent;")
// also enforce concrete line-height on textarea to stabilize caret metrics // also enforce concrete line-height on textarea to stabilize caret metrics
val existing = ta.getAttribute("style") ?: "" val existing = ta.getAttribute("style") ?: ""
if (!existing.contains("line-height") && !lineHeight.isNullOrBlank()) { if (!existing.contains("line-height") && !lineHeight.isNullOrBlank()) {

View File

@ -0,0 +1,55 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyngweb
import net.sergeych.lyng.format.LyngFormatConfig
import net.sergeych.lyng.highlight.TextRange
import net.sergeych.lyng.miniast.CompletionItem
import net.sergeych.lyng.tools.LyngAnalysisResult
import net.sergeych.lyng.tools.LyngLanguageTools
import net.sergeych.lyng.tools.LyngSymbolInfo
import net.sergeych.lyng.tools.LyngSymbolTarget
/**
* Thin JS-friendly facade for shared Lyng language tooling.
* Keeps web editor/site integrations consistent with IDE tooling behavior.
*/
object LyngWebTools {
suspend fun analyze(text: String, fileName: String = "<web>"): LyngAnalysisResult =
LyngLanguageTools.analyze(text, fileName)
suspend fun completions(text: String, offset: Int, analysis: LyngAnalysisResult? = null): List<CompletionItem> {
val a = analysis ?: analyze(text)
return LyngLanguageTools.completions(text, offset, a)
}
fun definitionAt(analysis: LyngAnalysisResult, offset: Int): LyngSymbolTarget? =
LyngLanguageTools.definitionAt(analysis, offset)
fun usagesAt(analysis: LyngAnalysisResult, offset: Int, includeDeclaration: Boolean = false): List<TextRange> =
LyngLanguageTools.usagesAt(analysis, offset, includeDeclaration)
fun docAt(analysis: LyngAnalysisResult, offset: Int): LyngSymbolInfo? =
LyngLanguageTools.docAt(analysis, offset)
fun format(text: String, config: LyngFormatConfig = LyngFormatConfig()): String =
LyngLanguageTools.format(text, config)
suspend fun disassembleSymbol(text: String, symbol: String): String =
LyngLanguageTools.disassembleSymbol(text, symbol)
}

View File

@ -120,6 +120,12 @@ fun ReferencePage() {
Div { Text("$kind ${d.name}$t") } Div { Text("$kind ${d.name}$t") }
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } } 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 -> { is MiniClassDecl -> {
Div { Text("class ${d.name}") } Div { Text("class ${d.name}") }
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } } d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
@ -145,6 +151,12 @@ fun ReferencePage() {
val staticStr = if (m.isStatic) "static " else "" val staticStr = if (m.isStatic) "static " else ""
Li { Text("${staticStr}${kindM} ${d.name}.${m.name}${ts}") } 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") }
}
} }
} }
} }

View File

@ -20,7 +20,15 @@ import kotlinx.coroutines.launch
import net.sergeych.lyng.LyngVersion import net.sergeych.lyng.LyngVersion
import net.sergeych.lyng.Script import net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError 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.EditorWithOverlay
import net.sergeych.lyngweb.LyngWebTools
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.dom.* import org.jetbrains.compose.web.dom.*
@Composable @Composable
@ -52,6 +60,15 @@ fun TryLyngPage(route: String) {
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) } 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() { fun runCode() {
if (running) return if (running) return
@ -59,6 +76,14 @@ fun TryLyngPage(route: String) {
output = null output = null
error = null error = null
extendedError = null extendedError = null
completionItems = emptyList()
completionOffset = null
docInfo = null
definitionTarget = null
usageRanges = emptyList()
diagnostics = emptyList()
disasmOutput = null
disasmError = null
scope.launch { scope.launch {
// keep this outside try so we can show partial prints if evaluation fails // keep this outside try so we can show partial prints if evaluation fails
val printed = StringBuilder() val printed = StringBuilder()
@ -156,6 +181,22 @@ fun TryLyngPage(route: String) {
runCode() 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 // Keep current initial size but allow the editor to grow with content
autoGrow = true 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 // Tips
P({ classes("text-muted", "small") }) { P({ classes("text-muted", "small") }) {
I({ classes("bi", "bi-info-circle", "me-1") }) 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.")
} }
} }
} }