attempt to add navigation to plugin (partial success)

This commit is contained in:
sergeych committed 2026-01-03 11:59:50 +01:00
1 parent 59aefc5bc2
commit bc6613ec01
23 files changed
+1498 -966

No files matched your search

@@ -1891,7 +1891,7 @@ class Compiler(
// create class
val className = nameToken.value
// @Suppress("UNUSED_VARIABLE") val defaultAccess = if (isStruct) AccessType.Var else AccessType.Initialization
// @Suppress("UNUSED_VARIABLE") val defaultAccess = if (isStruct) AccessType.Variable else AccessType.Initialization
// @Suppress("UNUSED_VARIABLE") val defaultVisibility = Visibility.Public
// create instance constructor
@@ -27,7 +27,7 @@ import net.sergeych.lyng.highlight.SimpleLyngHighlighter
import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.miniast.*
enum class SymbolKind { Class, Enum, Function, Val, Var, Param }
enum class SymbolKind { Class, Enum, Function, Value, Variable, Parameter }
data class Symbol(
val id: Int,
@@ -108,7 +108,7 @@ object Binder {
for (cf in d.ctorFields) {
val fs = source.offsetOf(cf.nameStart)
val fe = fs + cf.name.length
val kind = if (cf.mutable) SymbolKind.Var else SymbolKind.Val
val kind = if (cf.mutable) SymbolKind.Variable else SymbolKind.Value
val fieldSym = Symbol(nextId++, cf.name, kind, fs, fe, containerId = sym.id)
symbols += fieldSym
classes.last().fields += fieldSym.id
@@ -135,7 +135,7 @@ object Binder {
for (p in d.params) {
val ps = source.offsetOf(p.nameStart)
val pe = ps + p.name.length
val pk = SymbolKind.Param
val pk = SymbolKind.Parameter
val paramSym = Symbol(nextId++, p.name, pk, ps, pe, containerId = sym.id)
fnScope.locals += paramSym.id
symbols += paramSym
@@ -144,7 +144,7 @@ object Binder {
}
is MiniValDecl -> {
val (s, e) = nameOffsets(d.nameStart, d.name)
val kind = if (d.mutable) SymbolKind.Var else SymbolKind.Val
val kind = if (d.mutable) SymbolKind.Variable else SymbolKind.Value
val ownerClass = classContaining(s)
if (ownerClass != null) {
// class field
@@ -194,7 +194,7 @@ object Binder {
.maxByOrNull { it.rangeEnd - it.rangeStart }
if (containerFn != null) {
val fnSymId = containerFn.id
val kind = if (d.mutable) SymbolKind.Var else SymbolKind.Val
val kind = if (d.mutable) SymbolKind.Variable else SymbolKind.Value
val localSym = Symbol(nextId++, d.name, kind, s, e, containerId = fnSymId)
symbols += localSym
containerFn.locals += localSym.id
@@ -234,7 +234,7 @@ object Binder {
val inFn = functions.asSequence()
.filter { it.rangeEnd > it.rangeStart && nameStart >= it.rangeStart && nameStart <= it.rangeEnd }
.maxByOrNull { it.rangeEnd - it.rangeStart }
val kind = if (kw.equals("var", true)) SymbolKind.Var else SymbolKind.Val
val kind = if (kw.equals("var", true)) SymbolKind.Variable else SymbolKind.Value
if (inFn != null) {
val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = inFn.id)
symbols += localSym
@@ -62,42 +62,32 @@ object CompletionEngineLight {
StdlibDocsBootstrap.ensure()
val prefix = prefixAt(text, caret)
val mini = buildMiniAst(text)
// Build imported modules as a UNION of MiniAst-derived and textual extraction, always including stdlib
run {
// no-op block to keep local scope tidy
}
val fromMini: List<String> = mini?.let { DocLookupUtils.canonicalImportedModules(it) } ?: emptyList()
val fromText: List<String> = extractImportsFromText(text)
val imported: List<String> = LinkedHashSet<String>().apply {
fromMini.forEach { add(it) }
fromText.forEach { add(it) }
add("lyng.stdlib")
}.toList()
val imported: List<String> = DocLookupUtils.canonicalImportedModules(mini ?: return emptyList(), text)
val cap = 200
val out = ArrayList<CompletionItem>(64)
// Member context detection: dot immediately before caret or before current word start
val word = wordRangeAt(text, caret)
val memberDot = findDotLeft(text, word?.first ?: caret)
val word = DocLookupUtils.wordRangeAt(text, caret)
val memberDot = DocLookupUtils.findDotLeft(text, word?.first ?: caret)
if (memberDot != null) {
// 0) Try chained member call return type inference
guessReturnClassFromMemberCallBefore(text, memberDot, imported, mini)?.let { cls ->
DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDot, imported, mini)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
}
// 0a) Top-level call before dot
guessReturnClassFromTopLevelCallBefore(text, memberDot, imported, mini)?.let { cls ->
DocLookupUtils.guessReturnClassFromTopLevelCallBefore(text, memberDot, imported, mini)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
}
// 0b) Across-known-callees (Iterable/Iterator/List preference)
guessReturnClassAcrossKnownCallees(text, memberDot, imported, mini)?.let { cls ->
DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDot, imported, mini)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
}
// 1) Receiver inference fallback
(guessReceiverClassViaMini(mini, text, memberDot, imported) ?: guessReceiverClass(text, memberDot, imported, mini))?.let { cls ->
(DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDot, imported) ?: DocLookupUtils.guessReceiverClass(text, memberDot, imported, mini))?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
}
@@ -247,228 +237,6 @@ object CompletionEngineLight {
// --- Inference helpers (text-only, PSI-free) ---
private fun guessReceiverClassViaMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>): String? {
if (mini == null) return null
val i = prevNonWs(text, dotPos - 1)
if (i < 0) return null
val wordRange = wordRangeAt(text, i + 1) ?: return null
val ident = text.substring(wordRange.first, wordRange.second)
// 1) Global declarations in current file (val/var/fun/class/enum)
val d = mini.declarations.firstOrNull { it.name == ident }
if (d != null) {
return when (d) {
is MiniClassDecl -> d.name
is MiniEnumDecl -> d.name
is MiniValDecl -> simpleClassNameOf(d.type)
is MiniFunDecl -> simpleClassNameOf(d.returnType)
}
}
// 2) Recursive chaining: Base.ident.
val dotBefore = findDotLeft(text, wordRange.first)
if (dotBefore != null) {
val receiverClass = guessReceiverClassViaMini(mini, text, dotBefore, imported)
?: guessReceiverClass(text, dotBefore, imported, mini)
if (receiverClass != null) {
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, ident, mini)
if (resolved != null) {
val rt = when (val m = resolved.second) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type
else -> null
}
return simpleClassNameOf(rt)
}
}
}
// 3) Check if it's a known class (static access)
val classes = DocLookupUtils.aggregateClasses(imported, mini)
if (classes.containsKey(ident)) return ident
return null
}
private fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported, mini)?.let { return it }
var i = prevNonWs(text, dotPos - 1)
if (i >= 0) {
when (text[i]) {
'"' -> return "String"
']' -> return "List"
'}' -> return "Dict"
')' -> {
// Parenthesized expression: walk back to matching '(' and inspect the inner expression
var j = i - 1
var depth = 0
while (j >= 0) {
when (text[j]) {
')' -> depth++
'(' -> if (depth == 0) break else depth--
}
j--
}
if (j >= 0 && text[j] == '(') {
val innerS = (j + 1).coerceAtLeast(0)
val innerE = i.coerceAtMost(text.length)
if (innerS < innerE) {
val inner = text.substring(innerS, innerE).trim()
if (inner.startsWith('"') && inner.endsWith('"')) return "String"
if (inner.startsWith('[') && inner.endsWith(']')) return "List"
if (inner.startsWith('{') && inner.endsWith('}')) return "Dict"
}
}
}
}
// Numeric literal: decimal/int/hex/scientific
var j = i
var hasDigits = false
var hasDot = false
var hasExp = false
while (j >= 0) {
val ch = text[j]
if (ch.isDigit()) { hasDigits = true; j--; continue }
if (ch == '.') { hasDot = true; j--; continue }
if (ch == 'e' || ch == 'E') { hasExp = true; j--; if (j >= 0 && (text[j] == '+' || text[j] == '-')) j--; continue }
if (ch in listOf('x','X','a','b','c','d','f','A','B','C','D','F')) { j--; continue }
break
}
if (hasDigits) return if (hasDot || hasExp) "Real" else "Int"
// 3) this@Type or as Type
val identRange = wordRangeAt(text, i + 1)
if (identRange != null) {
val ident = text.substring(identRange.first, identRange.second)
// if it's "as Type", we want Type
var k = prevNonWs(text, identRange.first - 1)
if (k >= 1 && text[k] == 's' && text[k - 1] == 'a' && (k - 1 == 0 || !text[k - 2].isLetterOrDigit())) {
return ident
}
// if it's "this@Type", we want Type
if (k >= 0 && text[k] == '@') {
val k2 = prevNonWs(text, k - 1)
if (k2 >= 3 && text.substring(k2 - 3, k2 + 1) == "this") {
return ident
}
}
// 4) Check if it's a known class (static access)
val classes = DocLookupUtils.aggregateClasses(imported, mini)
if (classes.containsKey(ident)) return ident
}
}
return null
}
private fun guessReturnClassFromMemberCallBefore(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
var i = prevNonWs(text, dotPos - 1)
if (i < 0 || text[i] != ')') return null
i--
var depth = 0
while (i >= 0) {
when (text[i]) {
')' -> depth++
'(' -> if (depth == 0) break else depth--
}
i--
}
if (i < 0 || text[i] != '(') return null
var j = i - 1
while (j >= 0 && text[j].isWhitespace()) j--
val end = j + 1
while (j >= 0 && isIdentChar(text[j])) j--
val start = j + 1
if (start >= end) return null
val callee = text.substring(start, end)
var k = start - 1
while (k >= 0 && text[k].isWhitespace()) k--
if (k < 0 || text[k] != '.') return null
val prevDot = k
val receiverClass = guessReceiverClass(text, prevDot, imported, mini) ?: return null
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, callee, mini) ?: return null
val member = resolved.second
val ret = when (member) {
is MiniMemberFunDecl -> member.returnType
is MiniMemberValDecl -> member.type
is MiniInitDecl -> null
}
return simpleClassNameOf(ret)
}
private fun guessReturnClassFromTopLevelCallBefore(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
var i = prevNonWs(text, dotPos - 1)
if (i < 0 || text[i] != ')') return null
i--
var depth = 0
while (i >= 0) {
when (text[i]) {
')' -> depth++
'(' -> if (depth == 0) break else depth--
}
i--
}
if (i < 0 || text[i] != '(') return null
var j = i - 1
while (j >= 0 && text[j].isWhitespace()) j--
val end = j + 1
while (j >= 0 && isIdentChar(text[j])) j--
val start = j + 1
if (start >= end) return null
val callee = text.substring(start, end)
var k = start - 1
while (k >= 0 && text[k].isWhitespace()) k--
if (k >= 0 && text[k] == '.') return null // was a member call
for (mod in imported) {
val decls = BuiltinDocRegistry.docsForModule(mod)
val fn = decls.asSequence().filterIsInstance<MiniFunDecl>().firstOrNull { it.name == callee }
if (fn != null) return simpleClassNameOf(fn.returnType)
}
// Also check local declarations
mini?.declarations?.filterIsInstance<MiniFunDecl>()?.firstOrNull { it.name == callee }?.let { return simpleClassNameOf(it.returnType) }
return null
}
private fun guessReturnClassAcrossKnownCallees(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
var i = prevNonWs(text, dotPos - 1)
if (i < 0 || text[i] != ')') return null
i--
var depth = 0
while (i >= 0) {
when (text[i]) {
')' -> depth++
'(' -> if (depth == 0) break else depth--
}
i--
}
if (i < 0 || text[i] != '(') return null
var j = i - 1
while (j >= 0 && text[j].isWhitespace()) j--
val end = j + 1
while (j >= 0 && isIdentChar(text[j])) j--
val start = j + 1
if (start >= end) return null
val callee = text.substring(start, end)
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, callee, mini) ?: return null
val member = resolved.second
val ret = when (member) {
is MiniMemberFunDecl -> member.returnType
is MiniMemberValDecl -> member.type
is MiniInitDecl -> null
}
return simpleClassNameOf(ret)
}
private fun simpleClassNameOf(t: MiniTypeRef?): String? = when (t) {
null -> null
is MiniTypeName -> t.segments.lastOrNull()?.name
is MiniGenericType -> simpleClassNameOf(t.base)
is MiniFunctionType -> null
is MiniTypeVar -> null
}
// --- MiniAst and small utils ---
private suspend fun buildMiniAst(text: String): MiniScript? {
val sink = MiniAstBuilder()
return try {
@@ -499,42 +267,8 @@ object CompletionEngineLight {
private fun prefixAt(text: String, offset: Int): String {
val off = offset.coerceIn(0, text.length)
var i = (off - 1).coerceAtLeast(0)
while (i >= 0 && isIdentChar(text[i])) i--
while (i >= 0 && DocLookupUtils.isIdentChar(text[i])) i--
val start = i + 1
return if (start in 0..text.length && start <= off) text.substring(start, off) else ""
}
private fun wordRangeAt(text: String, offset: Int): Pair<Int, Int>? {
if (text.isEmpty()) return null
val off = offset.coerceIn(0, text.length)
var s = off
var e = off
while (s > 0 && isIdentChar(text[s - 1])) s--
while (e < text.length && isIdentChar(text[e])) e++
return if (s < e) s to e else null
}
private fun findDotLeft(text: String, offset: Int): Int? {
var i = (offset - 1).coerceAtLeast(0)
while (i >= 0 && text[i].isWhitespace()) i--
return if (i >= 0 && text[i] == '.') i else null
}
private fun prevNonWs(text: String, start: Int): Int {
var i = start.coerceAtMost(text.length - 1)
while (i >= 0 && text[i].isWhitespace()) i--
return i
}
private fun isIdentChar(c: Char): Boolean = c == '_' || c.isLetterOrDigit()
private fun extractImportsFromText(text: String): List<String> {
val result = LinkedHashSet<String>()
val re = Regex("^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", RegexOption.MULTILINE)
re.findAll(text).forEach { m ->
val raw = m.groupValues.getOrNull(1)?.trim().orEmpty()
if (raw.isNotEmpty()) result.add(if (raw.startsWith("lyng.")) raw else "lyng.$raw")
}
return result.toList()
}
}
@@ -20,27 +20,156 @@
*/
package net.sergeych.lyng.miniast
import net.sergeych.lyng.binding.BindingSnapshot
import net.sergeych.lyng.highlight.offsetOf
object DocLookupUtils {
fun findDeclarationAt(mini: MiniScript, offset: Int, name: String): Pair<String, String>? {
val src = mini.range.start.source
fun matches(p: net.sergeych.lyng.Pos, len: Int) = src.offsetOf(p).let { s -> offset >= s && offset < s + len }
for (d in mini.declarations) {
if (matches(d.nameStart, d.name.length)) {
val kind = when (d) {
is MiniFunDecl -> "Function"
is MiniClassDecl -> "Class"
is MiniEnumDecl -> "Enum"
is MiniValDecl -> if (d.mutable) "Variable" else "Value"
}
return d.name to kind
}
val members = when (d) {
is MiniFunDecl -> {
for (p in d.params) {
if (matches(p.nameStart, p.name.length)) return p.name to "Parameter"
}
emptyList()
}
is MiniEnumDecl -> {
// Enum entries don't have explicit nameStart yet, but we can check their text range if we had it.
// For now, heuristic: if offset is within enum range and matches an entry name.
// To be more precise, we should check that we are NOT at the 'enum' or enum name.
if (offset >= src.offsetOf(d.range.start) && offset <= src.offsetOf(d.range.end)) {
if (d.entries.contains(name) && !matches(d.nameStart, d.name.length)) {
// verify we are actually at the entry word in text
val off = src.offsetOf(d.range.start)
val end = src.offsetOf(d.range.end)
val text = src.text.substring(off, end)
// This is still a bit loose but better
return name to "EnumConstant"
}
}
emptyList()
}
is MiniClassDecl -> {
for (cf in d.ctorFields) {
if (matches(cf.nameStart, cf.name.length)) return cf.name to (if (cf.mutable) "Variable" else "Value")
}
for (cf in d.classFields) {
if (matches(cf.nameStart, cf.name.length)) return cf.name to (if (cf.mutable) "Variable" else "Value")
}
d.members
}
else -> emptyList()
}
for (m in members) {
if (matches(m.nameStart, m.name.length)) {
val kind = when (m) {
is MiniMemberFunDecl -> "Function"
is MiniMemberValDecl -> if (m.isStatic) "Value" else (if (m.mutable) "Variable" else "Value")
is MiniInitDecl -> "Initializer"
}
return m.name to kind
}
}
}
return null
}
fun findTypeByRange(mini: MiniScript?, name: String, startOffset: Int): MiniTypeRef? {
if (mini == null) return null
val src = mini.range.start.source
for (d in mini.declarations) {
if (d.name == name && src.offsetOf(d.nameStart) == startOffset) {
return when (d) {
is MiniValDecl -> d.type
is MiniFunDecl -> d.returnType
else -> null
}
}
if (d is MiniFunDecl) {
for (p in d.params) {
if (p.name == name && src.offsetOf(p.nameStart) == startOffset) return p.type
}
}
if (d is MiniClassDecl) {
for (m in d.members) {
if (m.name == name && src.offsetOf(m.nameStart) == startOffset) {
return when (m) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type
else -> null
}
}
}
for (cf in d.ctorFields) {
if (cf.name == name && src.offsetOf(cf.nameStart) == startOffset) return cf.type
}
for (cf in d.classFields) {
if (cf.name == name && src.offsetOf(cf.nameStart) == startOffset) return cf.type
}
}
}
return null
}
/**
* Convert MiniAst imports to fully-qualified module names expected by BuiltinDocRegistry.
* Heuristics:
* - If an import does not start with "lyng.", prefix it with "lyng." (e.g., "io.fs" -> "lyng.io.fs").
* - Keep original if it already starts with "lyng.".
* - Always include "lyng.stdlib" to make builtins available for docs.
* - If [sourceText] is provided, uses textual extraction as a fallback if MiniAst has no imports.
*/
fun canonicalImportedModules(mini: MiniScript): List<String> {
fun canonicalImportedModules(mini: MiniScript, sourceText: String? = null): List<String> {
val raw = mini.imports.map { it.segments.joinToString(".") { s -> s.name } }
val result = LinkedHashSet<String>()
for (name in raw) {
val canon = if (name.startsWith("lyng.")) name else "lyng.$name"
result.add(canon)
}
if (result.isEmpty() && sourceText != null) {
result.addAll(extractImportsFromText(sourceText))
}
// Always make stdlib available as a fallback context for common types,
// even when there are no explicit imports in the file
result.add("lyng.stdlib")
return result.toList()
}
fun extractImportsFromText(text: String): List<String> {
val result = LinkedHashSet<String>()
val re = Regex("^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", RegexOption.MULTILINE)
re.findAll(text).forEach { m ->
val raw = m.groupValues.getOrNull(1)?.trim().orEmpty()
if (raw.isNotEmpty()) {
val canon = if (raw.startsWith("lyng.")) raw else "lyng.$raw"
result.add(canon)
}
}
return result.toList()
}
fun aggregateClasses(importedModules: List<String>, localMini: MiniScript? = null): Map<String, MiniClassDecl> {
// Collect all class decls by name across modules, then merge duplicates by unioning members and bases.
val buckets = LinkedHashMap<String, MutableList<MiniClassDecl>>()
@@ -50,7 +179,7 @@ object DocLookupUtils {
buckets.getOrPut(cls.name) { mutableListOf() }.add(cls)
}
for (en in docs.filterIsInstance<MiniEnumDecl>()) {
buckets.getOrPut(en.name) { mutableListOf() }.add(en.toSyntheticClass())
buckets.getOrPut(en.name) { mutableListOf() }.add(enumToSyntheticClass(en))
}
}
@@ -61,7 +190,7 @@ object DocLookupUtils {
buckets.getOrPut(d.name) { mutableListOf() }.add(d)
}
is MiniEnumDecl -> {
val syn = d.toSyntheticClass()
val syn = enumToSyntheticClass(d)
buckets.getOrPut(d.name) { mutableListOf() }.add(syn)
}
else -> {}
@@ -140,10 +269,6 @@ object DocLookupUtils {
*/
fun guessClassFromCallBefore(text: String, dotPos: Int, importedModules: List<String>, localMini: MiniScript? = null): String? {
var i = (dotPos - 1).coerceAtLeast(0)
// Skip spaces
while (i >= 0 && text[i].isWhitespace()) i++
// Note: the previous line is wrong direction; correct implementation below
i = (dotPos - 1).coerceAtLeast(0)
while (i >= 0 && text[i].isWhitespace()) i--
if (i < 0 || text[i] != ')') return null
// Walk back to matching '(' accounting nested parentheses
@@ -172,27 +297,391 @@ object DocLookupUtils {
return if (classes.containsKey(name)) name else null
}
private fun isIdentChar(c: Char): Boolean = c == '_' || c.isLetterOrDigit()
fun guessReceiverClassViaMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>, binding: BindingSnapshot? = null): String? {
if (mini == null) return null
val i = prevNonWs(text, dotPos - 1)
if (i < 0) return null
val wordRange = wordRangeAt(text, i + 1) ?: return null
val ident = text.substring(wordRange.first, wordRange.second)
private fun MiniEnumDecl.toSyntheticClass(): MiniClassDecl {
// 0) Use binding if available for precise resolution
if (binding != null) {
val ref = binding.references.firstOrNull { wordRange.first >= it.start && wordRange.first < it.end }
if (ref != null) {
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
if (sym != null) {
val type = findTypeByRange(mini, sym.name, sym.declStart)
simpleClassNameOf(type)?.let { return it }
}
} else {
// Check if it's a declaration (e.g. static access to a class)
val sym = binding.symbols.firstOrNull { it.declStart == wordRange.first && it.name == ident }
if (sym != null) {
val type = findTypeByRange(mini, sym.name, sym.declStart)
simpleClassNameOf(type)?.let { return it }
// 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
}
}
}
// 1) Global declarations in current file (val/var/fun/class/enum)
val d = mini.declarations.firstOrNull { it.name == ident }
if (d != null) {
return when (d) {
is MiniClassDecl -> d.name
is MiniEnumDecl -> d.name
is MiniValDecl -> simpleClassNameOf(d.type)
is MiniFunDecl -> simpleClassNameOf(d.returnType)
}
}
// 2) Parameters in any function (best-effort fallback without scope mapping)
for (fd in mini.declarations.filterIsInstance<MiniFunDecl>()) {
for (p in fd.params) {
if (p.name == ident) return simpleClassNameOf(p.type)
}
}
// 3) Recursive chaining: Base.ident.
val dotBefore = findDotLeft(text, wordRange.first)
if (dotBefore != null) {
val receiverClass = guessReceiverClassViaMini(mini, text, dotBefore, imported, binding)
?: guessReceiverClass(text, dotBefore, imported, mini)
if (receiverClass != null) {
val resolved = resolveMemberWithInheritance(imported, receiverClass, ident, mini)
if (resolved != null) {
val rt = when (val m = resolved.second) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type
else -> null
}
return simpleClassNameOf(rt)
}
}
}
// 4) Check if it's a known class (static access)
val classes = aggregateClasses(imported, mini)
if (classes.containsKey(ident)) return ident
return null
}
fun guessReturnClassFromMemberCallBeforeMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>, binding: BindingSnapshot? = null): String? {
if (mini == null) return null
var i = prevNonWs(text, dotPos - 1)
if (i < 0 || text[i] != ')') return null
// back to matching '('
i--
var depth = 0
while (i >= 0) {
when (text[i]) {
')' -> depth++
'(' -> if (depth == 0) break else depth--
}
i--
}
if (i < 0 || text[i] != '(') return null
var j = i - 1
while (j >= 0 && text[j].isWhitespace()) j--
val end = j + 1
while (j >= 0 && isIdentChar(text[j])) j--
val start = j + 1
if (start >= end) return null
val callee = text.substring(start, end)
// Ensure member call: dot before callee
var k = start - 1
while (k >= 0 && text[k].isWhitespace()) k--
if (k < 0 || text[k] != '.') return null
val prevDot = k
// Resolve receiver class via MiniAst (ident like `x`)
val receiverClass = guessReceiverClassViaMini(mini, text, prevDot, imported, binding) ?: return null
// If receiver class is a locally declared class, resolve member on it
val localClass = mini.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == receiverClass }
if (localClass != null) {
val mm = localClass.members.firstOrNull { it.name == callee }
if (mm != null) {
val rt = when (mm) {
is MiniMemberFunDecl -> mm.returnType
is MiniMemberValDecl -> mm.type
else -> null
}
return simpleClassNameOf(rt)
} else {
// Try to scan class body text for method signature and extract return type
val sigs = scanLocalClassMembersFromText(mini, text, localClass)
val sig = sigs[callee]
if (sig != null && sig.typeText != null) return sig.typeText
}
}
// Else fallback to registry-based resolution (covers imported classes)
return resolveMemberWithInheritance(imported, receiverClass, callee, mini)?.second?.let { m ->
val rt = when (m) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type
is MiniInitDecl -> null
}
simpleClassNameOf(rt)
}
}
data class ScannedSig(val kind: String, val params: List<String>?, val typeText: String?)
fun scanLocalClassMembersFromText(mini: MiniScript, text: String, cls: MiniClassDecl): Map<String, ScannedSig> {
val src = mini.range.start.source
val start = src.offsetOf(cls.bodyRange?.start ?: cls.range.start)
val end = src.offsetOf(cls.bodyRange?.end ?: cls.range.end).coerceAtMost(text.length)
if (start !in 0..end) return emptyMap()
val body = text.substring(start, end)
val map = LinkedHashMap<String, ScannedSig>()
// fun name(params): Type
val funRe = Regex("^\\s*fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(([^)]*)\\)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?", RegexOption.MULTILINE)
for (m in funRe.findAll(body)) {
val name = m.groupValues.getOrNull(1) ?: continue
val params = m.groupValues.getOrNull(2)?.split(',')?.mapNotNull { it.trim().takeIf { it.isNotEmpty() } } ?: emptyList()
val type = m.groupValues.getOrNull(3)?.takeIf { it.isNotBlank() }
map[name] = ScannedSig("fun", params, type)
}
// val/var name: Type
val valRe = Regex("^\\s*(val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?", RegexOption.MULTILINE)
for (m in valRe.findAll(body)) {
val kind = m.groupValues.getOrNull(1) ?: continue
val name = m.groupValues.getOrNull(2) ?: continue
val type = m.groupValues.getOrNull(3)?.takeIf { it.isNotBlank() }
if (!map.containsKey(name)) map[name] = ScannedSig(kind, null, type)
}
return map
}
fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
guessClassFromCallBefore(text, dotPos, imported, mini)?.let { return it }
var i = prevNonWs(text, dotPos - 1)
if (i >= 0) {
when (text[i]) {
'"' -> return "String"
']' -> return "List"
'}' -> return "Dict"
')' -> {
// Parenthesized expression: walk back to matching '(' and inspect the inner expression
var j = i - 1
var depth = 0
while (j >= 0) {
when (text[j]) {
')' -> depth++
'(' -> if (depth == 0) break else depth--
}
j--
}
if (j >= 0 && text[j] == '(') {
val innerS = (j + 1).coerceAtLeast(0)
val innerE = i.coerceAtMost(text.length)
if (innerS < innerE) {
val inner = text.substring(innerS, innerE).trim()
if (inner.startsWith('"') && inner.endsWith('"')) return "String"
if (inner.startsWith('[') && inner.endsWith(']')) return "List"
if (inner.startsWith('{') && inner.endsWith('}')) return "Dict"
}
}
}
}
// Numeric literal: decimal/int/hex/scientific
var j = i
var hasDigits = false
var hasDot = false
var hasExp = false
while (j >= 0) {
val ch = text[j]
if (ch.isDigit()) {
hasDigits = true; j--; continue
}
if (ch == '.') {
hasDot = true; j--; continue
}
if (ch == 'e' || ch == 'E') {
hasExp = true; j--; if (j >= 0 && (text[j] == '+' || text[j] == '-')) j--; continue
}
if (ch in listOf('x', 'X', 'a', 'b', 'c', 'd', 'f', 'A', 'B', 'C', 'D', 'F')) {
j--; continue
}
break
}
if (hasDigits) return if (hasDot || hasExp) "Real" else "Int"
// 3) this@Type or as Type
val identRange = wordRangeAt(text, i + 1)
if (identRange != null) {
val ident = text.substring(identRange.first, identRange.second)
// if it's "as Type", we want Type
var k = prevNonWs(text, identRange.first - 1)
if (k >= 1 && text[k] == 's' && text[k - 1] == 'a' && (k - 1 == 0 || !text[k - 2].isLetterOrDigit())) {
return ident
}
// if it's "this@Type", we want Type
if (k >= 0 && text[k] == '@') {
val k2 = prevNonWs(text, k - 1)
if (k2 >= 3 && text.substring(k2 - 3, k2 + 1) == "this") {
return ident
}
}
// 4) Check if it's a known class (static access)
val classes = aggregateClasses(imported, mini)
if (classes.containsKey(ident)) return ident
}
}
return null
}
fun guessReturnClassFromMemberCallBefore(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
var i = prevNonWs(text, dotPos - 1)
if (i < 0 || text[i] != ')') return null
i--
var depth = 0
while (i >= 0) {
when (text[i]) {
')' -> depth++
'(' -> if (depth == 0) break else depth--
}
i--
}
if (i < 0 || text[i] != '(') return null
var j = i - 1
while (j >= 0 && text[j].isWhitespace()) j--
val end = j + 1
while (j >= 0 && isIdentChar(text[j])) j--
val start = j + 1
if (start >= end) return null
val callee = text.substring(start, end)
var k = start - 1
while (k >= 0 && text[k].isWhitespace()) k--
if (k < 0 || text[k] != '.') return null
val prevDot = k
val receiverClass = guessReceiverClass(text, prevDot, imported, mini) ?: return null
val resolved = resolveMemberWithInheritance(imported, receiverClass, callee, mini) ?: return null
val member = resolved.second
val ret = when (member) {
is MiniMemberFunDecl -> member.returnType
is MiniMemberValDecl -> member.type
is MiniInitDecl -> null
}
return simpleClassNameOf(ret)
}
fun guessReturnClassFromTopLevelCallBefore(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
var i = prevNonWs(text, dotPos - 1)
if (i < 0 || text[i] != ')') return null
i--
var depth = 0
while (i >= 0) {
when (text[i]) {
')' -> depth++
'(' -> if (depth == 0) break else depth--
}
i--
}
if (i < 0 || text[i] != '(') return null
var j = i - 1
while (j >= 0 && text[j].isWhitespace()) j--
val end = j + 1
while (j >= 0 && isIdentChar(text[j])) j--
val start = j + 1
if (start >= end) return null
val callee = text.substring(start, end)
var k = start - 1
while (k >= 0 && text[k].isWhitespace()) k--
if (k >= 0 && text[k] == '.') return null // was a member call
for (mod in imported) {
val decls = BuiltinDocRegistry.docsForModule(mod)
val fn = decls.asSequence().filterIsInstance<MiniFunDecl>().firstOrNull { it.name == callee }
if (fn != null) return simpleClassNameOf(fn.returnType)
}
// Also check local declarations
mini?.declarations?.filterIsInstance<MiniFunDecl>()?.firstOrNull { it.name == callee }?.let { return simpleClassNameOf(it.returnType) }
return null
}
fun guessReturnClassAcrossKnownCallees(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
var i = prevNonWs(text, dotPos - 1)
if (i < 0 || text[i] != ')') return null
i--
var depth = 0
while (i >= 0) {
when (text[i]) {
')' -> depth++
'(' -> if (depth == 0) break else depth--
}
i--
}
if (i < 0 || text[i] != '(') return null
var j = i - 1
while (j >= 0 && text[j].isWhitespace()) j--
val end = j + 1
while (j >= 0 && isIdentChar(text[j])) j--
val start = j + 1
if (start >= end) return null
val callee = text.substring(start, end)
val resolved = findMemberAcrossClasses(imported, callee, mini) ?: return null
val member = resolved.second
val ret = when (member) {
is MiniMemberFunDecl -> member.returnType
is MiniMemberValDecl -> member.type
is MiniInitDecl -> null
}
return simpleClassNameOf(ret)
}
fun simpleClassNameOf(t: MiniTypeRef?): String? = when (t) {
null -> null
is MiniTypeName -> t.segments.lastOrNull()?.name
is MiniGenericType -> simpleClassNameOf(t.base)
is MiniFunctionType -> null
is MiniTypeVar -> null
}
fun findDotLeft(text: String, offset: Int): Int? {
var i = (offset - 1).coerceAtLeast(0)
while (i >= 0 && text[i].isWhitespace()) i--
return if (i >= 0 && text[i] == '.') i else null
}
fun prevNonWs(text: String, start: Int): Int {
var i = start.coerceAtMost(text.length - 1)
while (i >= 0 && text[i].isWhitespace()) i--
return i
}
fun wordRangeAt(text: String, offset: Int): Pair<Int, Int>? {
if (text.isEmpty()) return null
val off = offset.coerceIn(0, text.length)
var s = off
var e = off
while (s > 0 && isIdentChar(text[s - 1])) s--
while (e < text.length && isIdentChar(text[e])) e++
return if (s < e) s to e else null
}
fun isIdentChar(c: Char): Boolean = c == '_' || c.isLetterOrDigit()
fun enumToSyntheticClass(en: MiniEnumDecl): MiniClassDecl {
val staticMembers = mutableListOf<MiniMemberDecl>()
// entries: List
staticMembers.add(MiniMemberValDecl(range, "entries", false, null, null, nameStart, isStatic = true))
staticMembers.add(MiniMemberValDecl(en.range, "entries", false, null, null, en.nameStart, isStatic = true))
// valueOf(name: String): Enum
staticMembers.add(MiniMemberFunDecl(range, "valueOf", listOf(MiniParam("name", null, nameStart)), null, null, nameStart, isStatic = true))
staticMembers.add(MiniMemberFunDecl(en.range, "valueOf", listOf(MiniParam("name", null, en.nameStart)), null, null, en.nameStart, isStatic = true))
// Also add each entry as a static member (const)
for (entry in entries) {
staticMembers.add(MiniMemberValDecl(range, entry, false, MiniTypeName(range, listOf(MiniTypeName.Segment(name, range)), false), null, nameStart, isStatic = true))
for (entry in en.entries) {
staticMembers.add(MiniMemberValDecl(en.range, entry, false, MiniTypeName(en.range, listOf(MiniTypeName.Segment(en.name, en.range)), false), null, en.nameStart, isStatic = true))
}
return MiniClassDecl(
range = range,
name = name,
range = en.range,
name = en.name,
bases = listOf("Enum"),
bodyRange = null,
doc = doc,
nameStart = nameStart,
doc = en.doc,
nameStart = en.nameStart,
members = staticMembers
)
}