plugin with type declarations, collection types and much better type tracking for autocomplete

This commit is contained in:
Sergey Chernov 2026-01-07 02:55:47 +01:00
parent aba0048a83
commit fe5dded7af
17 changed files with 1195 additions and 182 deletions

View File

@ -45,6 +45,8 @@ dependencies {
// Tests for IntelliJ Platform fixtures rely on JUnit 3/4 API (junit.framework.TestCase) // Tests for IntelliJ Platform fixtures rely on JUnit 3/4 API (junit.framework.TestCase)
// Add JUnit 4 which contains the JUnit 3 compatibility classes used by BasePlatformTestCase/UsefulTestCase // Add JUnit 4 which contains the JUnit 3 compatibility classes used by BasePlatformTestCase/UsefulTestCase
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2")
testImplementation("org.opentest4j:opentest4j:1.3.0")
} }
intellij { intellij {

View File

@ -102,7 +102,7 @@ class LyngCompletionContributor : CompletionContributor() {
// 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 {
runBlocking { CompletionEngineLight.completeSuspend(text, caret, mini) } runBlocking { CompletionEngineLight.completeSuspend(text, caret, mini, binding) }
} catch (t: Throwable) { } catch (t: Throwable) {
if (DEBUG_COMPLETION) log.warn("[LYNG_DEBUG] Engine completion failed: ${t.message}") if (DEBUG_COMPLETION) log.warn("[LYNG_DEBUG] Engine completion failed: ${t.message}")
emptyList() emptyList()
@ -185,33 +185,51 @@ class LyngCompletionContributor : CompletionContributor() {
?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini) ?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini) ?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini)
if (!inferredClass.isNullOrBlank()) { if (!inferredClass.isNullOrBlank()) {
val ext = BuiltinDocRegistry.extensionMemberNamesFor(inferredClass) val ext = DocLookupUtils.collectExtensionMemberNames(imported, inferredClass, mini)
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${'$'}{ext}") if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${ext}")
for (name in ext) { for (name in ext) {
if (existing.contains(name)) continue if (existing.contains(name)) continue
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name, mini) val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name, mini)
if (resolved != null) { if (resolved != null) {
when (val member = resolved.second) { val m = resolved.second
val builder = when (m) {
is MiniMemberFunDecl -> { is MiniMemberFunDecl -> {
val params = member.params.joinToString(", ") { it.name } val params = m.params.joinToString(", ") { it.name }
val ret = typeOf(member.returnType) val ret = typeOf(m.returnType)
val builder = LookupElementBuilder.create(name) LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method) .withIcon(AllIcons.Nodes.Method)
.withTailText("(${ '$' }params)", true) .withTailText("($params)", true)
.withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler)
}
is MiniFunDecl -> {
val params = m.params.joinToString(", ") { it.name }
val ret = typeOf(m.returnType)
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("($params)", true)
.withTypeText(ret, true) .withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
emit(builder)
existing.add(name)
} }
is MiniMemberValDecl -> { is MiniMemberValDecl -> {
val builder = LookupElementBuilder.create(name) LookupElementBuilder.create(name)
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field) .withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(member.type), true) .withTypeText(typeOf(m.type), true)
emit(builder) }
existing.add(name) is MiniValDecl -> {
LookupElementBuilder.create(name)
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(m.type), true)
}
else -> {
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("()", true)
.withInsertHandler(ParenInsertHandler)
} }
is MiniInitDecl -> {}
} }
emit(builder)
existing.add(name)
} else { } else {
// Fallback: emit simple method name without detailed types // Fallback: emit simple method name without detailed types
val builder = LookupElementBuilder.create(name) val builder = LookupElementBuilder.create(name)
@ -455,27 +473,44 @@ class LyngCompletionContributor : CompletionContributor() {
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, name, mini) val resolved = DocLookupUtils.findMemberAcrossClasses(imported, name, mini)
if (resolved != null) { if (resolved != null) {
val member = resolved.second val member = resolved.second
when (member) { val builder = when (member) {
is MiniMemberFunDecl -> { is MiniMemberFunDecl -> {
val params = member.params.joinToString(", ") { it.name } val params = member.params.joinToString(", ") { it.name }
val ret = typeOf(member.returnType) val ret = typeOf(member.returnType)
val builder = LookupElementBuilder.create(name) LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method) .withIcon(AllIcons.Nodes.Method)
.withTailText("(${params})", true) .withTailText("($params)", true)
.withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler)
}
is MiniFunDecl -> {
val params = member.params.joinToString(", ") { it.name }
val ret = typeOf(member.returnType)
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("($params)", true)
.withTypeText(ret, true) .withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
emit(builder)
already.add(name)
} }
is MiniMemberValDecl -> { is MiniMemberValDecl -> {
val builder = LookupElementBuilder.create(name) LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Field) .withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(member.type), true) .withTypeText(typeOf(member.type), true)
emit(builder)
already.add(name)
} }
is MiniInitDecl -> {} is MiniValDecl -> {
LookupElementBuilder.create(name)
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(member.type), true)
}
else -> {
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("()", true)
.withInsertHandler(ParenInsertHandler)
}
} }
emit(builder)
already.add(name)
} else { } else {
// Synthetic fallback: method without detailed params/types to improve UX in absence of docs // Synthetic fallback: method without detailed params/types to improve UX in absence of docs
val isProperty = name in setOf("size", "length") val isProperty = name in setOf("size", "length")
@ -504,29 +539,46 @@ class LyngCompletionContributor : CompletionContributor() {
// Try to resolve full signature via registry first to get params and return type // Try to resolve full signature via registry first to get params and return type
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name, mini) val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name, mini)
if (resolved != null) { if (resolved != null) {
when (val member = resolved.second) { val m = resolved.second
val builder = when (m) {
is MiniMemberFunDecl -> { is MiniMemberFunDecl -> {
val params = member.params.joinToString(", ") { it.name } val params = m.params.joinToString(", ") { it.name }
val ret = typeOf(member.returnType) val ret = typeOf(m.returnType)
val builder = LookupElementBuilder.create(name) LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method) .withIcon(AllIcons.Nodes.Method)
.withTailText("(${params})", true) .withTailText("($params)", true)
.withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler)
}
is MiniFunDecl -> {
val params = m.params.joinToString(", ") { it.name }
val ret = typeOf(m.returnType)
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("($params)", true)
.withTypeText(ret, true) .withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
emit(builder)
already.add(name)
continue
} }
is MiniMemberValDecl -> { is MiniMemberValDecl -> {
val builder = LookupElementBuilder.create(name) LookupElementBuilder.create(name)
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field) .withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(member.type), true) .withTypeText(typeOf(m.type), true)
emit(builder) }
already.add(name) is MiniValDecl -> {
continue LookupElementBuilder.create(name)
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(m.type), true)
}
else -> {
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("()", true)
.withInsertHandler(ParenInsertHandler)
} }
is MiniInitDecl -> {}
} }
emit(builder)
already.add(name)
continue
} }
// Fallback: emit without detailed types if we couldn't resolve // Fallback: emit without detailed types if we couldn't resolve
val builder = LookupElementBuilder.create(name) val builder = LookupElementBuilder.create(name)

View File

@ -68,6 +68,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
// 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 mini = LyngAstManager.getMiniAst(file) ?: return null
val miniSource = mini.range.start.source val miniSource = mini.range.start.source
val imported = DocLookupUtils.canonicalImportedModules(mini, text)
// 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
@ -78,7 +79,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
if (d.name == name) { if (d.name == name) {
val s: Int = miniSource.offsetOf(d.nameStart) val s: Int = miniSource.offsetOf(d.nameStart)
if (s <= offset && s + d.name.length > offset) { if (s <= offset && s + d.name.length > offset) {
return renderDeclDoc(d) return renderDeclDoc(d, text, mini, imported)
} }
} }
// Handle members if it was a member // Handle members if it was a member
@ -105,6 +106,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
name = cf.name, name = cf.name,
mutable = cf.mutable, mutable = cf.mutable,
type = cf.type, type = cf.type,
initRange = null,
doc = null, doc = null,
nameStart = cf.nameStart nameStart = cf.nameStart
) )
@ -122,6 +124,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
name = cf.name, name = cf.name,
mutable = cf.mutable, mutable = cf.mutable,
type = cf.type, type = cf.type,
initRange = null,
doc = null, doc = null,
nameStart = cf.nameStart nameStart = cf.nameStart
) )
@ -171,7 +174,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
} }
} }
} }
if (dsFound != null) return renderDeclDoc(dsFound) if (dsFound != null) return renderDeclDoc(dsFound, text, mini, imported)
// Check parameters // Check parameters
mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn -> mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
@ -209,6 +212,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
name = cf.name, name = cf.name,
mutable = cf.mutable, mutable = cf.mutable,
type = cf.type, type = cf.type,
initRange = null,
doc = null, doc = null,
nameStart = cf.nameStart nameStart = cf.nameStart
) )
@ -226,6 +230,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
name = cf.name, name = cf.name,
mutable = cf.mutable, mutable = cf.mutable,
type = cf.type, type = cf.type,
initRange = null,
doc = null, doc = null,
nameStart = cf.nameStart nameStart = cf.nameStart
) )
@ -308,6 +313,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniInitDecl -> null is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> 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}")
@ -318,7 +327,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
// 4) As a fallback, if the caret is on an identifier text that matches any declaration name, show that // 4) As a fallback, if the caret is on an identifier text that matches any declaration name, show that
mini.declarations.firstOrNull { it.name == ident }?.let { mini.declarations.firstOrNull { it.name == ident }?.let {
log.info("[LYNG_DEBUG] QuickDoc: fallback by name '${it.name}' kind=${it::class.simpleName}") log.info("[LYNG_DEBUG] QuickDoc: fallback by name '${it.name}' kind=${it::class.simpleName}")
return renderDeclDoc(it) return renderDeclDoc(it, text, mini, imported)
} }
// 4) Consult BuiltinDocRegistry for imported modules (top-level and class members) // 4) Consult BuiltinDocRegistry for imported modules (top-level and class members)
@ -338,13 +347,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
if (arity != null && chosen.params.size != arity && matches.size > 1) { if (arity != null && chosen.params.size != arity && matches.size > 1) {
return renderOverloads(ident, matches) return renderOverloads(ident, matches)
} }
return renderDeclDoc(chosen) return renderDeclDoc(chosen, text, mini, imported)
} }
// Also allow values/consts // Also allow values/consts
docs.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) } docs.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
// And classes/enums // And classes/enums
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) } 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) } docs.filterIsInstance<MiniEnumDecl>().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") {
@ -364,6 +373,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniInitDecl -> null is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
} }
} }
} else { } else {
@ -383,6 +396,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniInitDecl -> null is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
} }
} }
} else { } else {
@ -396,6 +413,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniInitDecl -> null is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
} }
} }
} }
@ -421,6 +442,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniInitDecl -> null is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
} }
} }
} }
@ -468,12 +493,16 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return contextElement ?: file.findElementAt(targetOffset) return contextElement ?: file.findElementAt(targetOffset)
} }
private fun renderDeclDoc(d: MiniDecl): String { private fun renderDeclDoc(d: MiniDecl, text: String, mini: MiniScript, imported: List<String>): String {
val title = when (d) { val title = when (d) {
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 MiniValDecl -> if (d.mutable) "var ${d.name}${typeOf(d.type)}" else "val ${d.name}${typeOf(d.type)}" is MiniValDecl -> {
val t = d.type ?: DocLookupUtils.inferTypeRefForVal(d, text, imported, mini)
val typeStr = if (t == null) ": Object?" else typeOf(t)
if (d.mutable) "var ${d.name}${typeStr}" else "val ${d.name}${typeStr}"
}
} }
// Show full detailed documentation, not just the summary // Show full detailed documentation, not just the summary
val raw = d.doc?.raw val raw = d.doc?.raw
@ -506,7 +535,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
} }
private fun renderMemberValDoc(className: String, m: MiniMemberValDecl): String { private fun renderMemberValDoc(className: String, m: MiniMemberValDecl): String {
val ts = typeOf(m.type) val ts = if (m.type == null) ": Object?" else typeOf(m.type)
val kind = if (m.mutable) "var" else "val" val kind = if (m.mutable) "var" else "val"
val staticStr = if (m.isStatic) "static " else "" val staticStr = if (m.isStatic) "static " else ""
val title = "${staticStr}${kind} $className.${m.name}${ts}" val title = "${staticStr}${kind} $className.${m.name}${ts}"
@ -527,7 +556,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
} }
is MiniFunctionType -> ": (..) -> ..${if (t.nullable) "?" else ""}" is MiniFunctionType -> ": (..) -> ..${if (t.nullable) "?" else ""}"
is MiniTypeVar -> ": ${t.name}${if (t.nullable) "?" else ""}" is MiniTypeVar -> ": ${t.name}${if (t.nullable) "?" else ""}"
null -> "" null -> ": Object?"
} }
private fun signatureOf(fn: MiniFunDecl): String { private fun signatureOf(fn: MiniFunDecl): String {

View File

@ -63,6 +63,10 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
is MiniMemberFunDecl -> "Function" is MiniMemberFunDecl -> "Function"
is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value" is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value"
is MiniInitDecl -> "Initializer" is MiniInitDecl -> "Initializer"
is MiniFunDecl -> "Function"
is MiniValDecl -> if (member.mutable) "Variable" else "Value"
is MiniClassDecl -> "Class"
is MiniEnumDecl -> "Enum"
} }
results.add(PsiElementResolveResult(LyngDeclarationElement(it, member.name, kind))) results.add(PsiElementResolveResult(LyngDeclarationElement(it, member.name, kind)))
} }

View File

@ -23,7 +23,7 @@ actual object ArgBuilderProvider {
private val tl = object : ThreadLocal<AndroidArgsBuilder>() { private val tl = object : ThreadLocal<AndroidArgsBuilder>() {
override fun initialValue(): AndroidArgsBuilder = AndroidArgsBuilder() override fun initialValue(): AndroidArgsBuilder = AndroidArgsBuilder()
} }
actual fun acquire(): ArgsBuilder = tl.get() actual fun acquire(): ArgsBuilder = tl.get()!!
} }
private class AndroidArgsBuilder : ArgsBuilder { private class AndroidArgsBuilder : ArgsBuilder {

View File

@ -973,7 +973,10 @@ class Compiler(
private fun parseTypeDeclarationWithMini(): Pair<TypeDecl, MiniTypeRef?> { private fun parseTypeDeclarationWithMini(): Pair<TypeDecl, MiniTypeRef?> {
// Only parse a type if a ':' follows; otherwise keep current behavior // Only parse a type if a ':' follows; otherwise keep current behavior
if (!cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) return Pair(TypeDecl.TypeAny, null) if (!cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) return Pair(TypeDecl.TypeAny, null)
return parseTypeExpressionWithMini()
}
private fun parseTypeExpressionWithMini(): Pair<TypeDecl, MiniTypeRef> {
// Parse a qualified base name: ID ('.' ID)* // Parse a qualified base name: ID ('.' ID)*
val segments = mutableListOf<MiniTypeName.Segment>() val segments = mutableListOf<MiniTypeName.Segment>()
var first = true var first = true
@ -1009,41 +1012,28 @@ class Compiler(
else MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable) else MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable)
} }
// Optional generic arguments: '<' Type (',' Type)* '>' — single-level only (no nested generics for now) // Optional generic arguments: '<' Type (',' Type)* '>'
var args: MutableList<MiniTypeRef>? = null var miniArgs: MutableList<MiniTypeRef>? = null
var semArgs: MutableList<TypeDecl>? = null
val afterBasePos = cc.savePos() val afterBasePos = cc.savePos()
if (cc.skipTokenOfType(Token.Type.LT, isOptional = true)) { if (cc.skipTokenOfType(Token.Type.LT, isOptional = true)) {
args = mutableListOf() miniArgs = mutableListOf()
semArgs = mutableListOf()
do { do {
// Parse argument as simple or qualified type (single level), with optional nullable '?' val (argSem, argMini) = parseTypeExpressionWithMini()
val argSegs = mutableListOf<MiniTypeName.Segment>() miniArgs += argMini
var argFirst = true semArgs += argSem
val argStart = cc.currentPos()
while (true) {
val idTok = if (argFirst) cc.requireToken(
Token.Type.ID,
"type argument name expected"
) else cc.requireToken(Token.Type.ID, "identifier expected after '.' in type argument")
argFirst = false
argSegs += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos))
val p = cc.savePos()
val tt = cc.next()
if (tt.type == Token.Type.DOT) continue else {
cc.restorePos(p); break
}
}
val argNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
val argEnd = cc.currentPos()
val argRef = MiniTypeName(MiniRange(argStart, argEnd), argSegs.toList(), nullable = argNullable)
args += argRef
val sep = cc.next() val sep = cc.next()
when (sep.type) { if (sep.type == Token.Type.COMMA) {
Token.Type.COMMA -> { /* continue */ // continue
} } else if (sep.type == Token.Type.GT) {
break
Token.Type.GT -> break } else if (sep.type == Token.Type.SHR) {
else -> sep.raiseSyntax("expected ',' or '>' in generic arguments") cc.pushPendingGT()
break
} else {
sep.raiseSyntax("expected ',' or '>' in generic arguments")
} }
} while (true) } while (true)
lastEnd = cc.currentPos() lastEnd = cc.currentPos()
@ -1055,10 +1045,11 @@ class Compiler(
val isNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true) val isNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
val endPos = cc.currentPos() val endPos = cc.currentPos()
val miniRef = buildBaseRef(if (args != null) endPos else lastEnd, args, isNullable) val miniRef = buildBaseRef(if (miniArgs != null) endPos else lastEnd, miniArgs, isNullable)
// Semantic: keep simple for now, just use qualified base name with nullable flag // Semantic: keep simple for now, just use qualified base name with nullable flag
val qualified = segments.joinToString(".") { it.name } val qualified = segments.joinToString(".") { it.name }
val sem = TypeDecl.Simple(qualified, isNullable) val sem = if (semArgs != null) TypeDecl.Generic(qualified, semArgs, isNullable)
else TypeDecl.Simple(qualified, isNullable)
return Pair(sem, miniRef) return Pair(sem, miniRef)
} }
@ -1474,7 +1465,9 @@ class Compiler(
"init" -> { "init" -> {
if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
miniSink?.onEnterFunction(null)
val block = parseBlock() val block = parseBlock()
miniSink?.onExitFunction(cc.currentPos())
lastParsedBlockRange?.let { range -> lastParsedBlockRange?.let { range ->
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos)) miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
} }
@ -2714,8 +2707,7 @@ class Compiler(
val declDocLocal = pendingDeclDoc val declDocLocal = pendingDeclDoc
val outerLabel = lastLabel val outerLabel = lastLabel
// Emit MiniFunDecl before body parsing (body range unknown yet) val node = run {
run {
val params = argsDeclaration.params.map { p -> val params = argsDeclaration.params.map { p ->
MiniParam( MiniParam(
name = p.name, name = p.name,
@ -2737,8 +2729,10 @@ class Compiler(
) )
miniSink?.onFunDecl(node) miniSink?.onFunDecl(node)
pendingDeclDoc = null pendingDeclDoc = null
node
} }
miniSink?.onEnterFunction(node)
return inCodeContext(CodeContext.Function(name)) { return inCodeContext(CodeContext.Function(name)) {
cc.labels.add(name) cc.labels.add(name)
outerLabel?.let { cc.labels.add(it) } outerLabel?.let { cc.labels.add(it) }
@ -2941,6 +2935,7 @@ class Compiler(
isExtern = actualExtern isExtern = actualExtern
) )
miniSink?.onFunDecl(node) miniSink?.onFunDecl(node)
miniSink?.onExitFunction(cc.currentPos())
} }
} }
@ -3186,9 +3181,11 @@ class Compiler(
while (true) { while (true) {
val t = cc.skipWsTokens() val t = cc.skipWsTokens()
if (t.isId("get")) { if (t.isId("get")) {
val getStart = cc.currentPos()
cc.next() // consume 'get' cc.next() // consume 'get'
cc.requireToken(Token.Type.LPAREN) cc.requireToken(Token.Type.LPAREN)
cc.requireToken(Token.Type.RPAREN) cc.requireToken(Token.Type.RPAREN)
miniSink?.onEnterFunction(null)
getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens() cc.skipWsTokens()
parseBlock() parseBlock()
@ -3200,11 +3197,14 @@ class Compiler(
} else { } else {
throw ScriptError(cc.current().pos, "Expected { or = after get()") throw ScriptError(cc.current().pos, "Expected { or = after get()")
} }
miniSink?.onExitFunction(cc.currentPos())
} else if (t.isId("set")) { } else if (t.isId("set")) {
val setStart = cc.currentPos()
cc.next() // consume 'set' cc.next() // consume 'set'
cc.requireToken(Token.Type.LPAREN) cc.requireToken(Token.Type.LPAREN)
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name") val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
cc.requireToken(Token.Type.RPAREN) cc.requireToken(Token.Type.RPAREN)
miniSink?.onEnterFunction(null)
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens() cc.skipWsTokens()
val body = parseBlock() val body = parseBlock()
@ -3226,6 +3226,7 @@ class Compiler(
} else { } else {
throw ScriptError(cc.current().pos, "Expected { or = after set(...)") throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
} }
miniSink?.onExitFunction(cc.currentPos())
} else if (t.isId("private") || t.isId("protected")) { } else if (t.isId("private") || t.isId("protected")) {
val vis = if (t.isId("private")) Visibility.Private else Visibility.Protected val vis = if (t.isId("private")) Visibility.Private else Visibility.Protected
val mark = cc.savePos() val mark = cc.savePos()
@ -3237,6 +3238,7 @@ class Compiler(
cc.next() // consume '(' cc.next() // consume '('
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name") val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
cc.requireToken(Token.Type.RPAREN) cc.requireToken(Token.Type.RPAREN)
miniSink?.onEnterFunction(null)
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens() cc.skipWsTokens()
val body = parseBlock() val body = parseBlock()
@ -3261,6 +3263,7 @@ class Compiler(
} else { } else {
throw ScriptError(cc.current().pos, "Expected { or = after set(...)") throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
} }
miniSink?.onExitFunction(cc.currentPos())
} }
} else { } else {
cc.restorePos(mark) cc.restorePos(mark)

View File

@ -34,18 +34,34 @@ class CompilerContext(val tokens: List<Token>) {
} }
var currentIndex = 0 var currentIndex = 0
private var pendingGT = 0
fun hasNext() = currentIndex < tokens.size fun hasNext() = currentIndex < tokens.size || pendingGT > 0
fun hasPrevious() = currentIndex > 0 fun hasPrevious() = currentIndex > 0
fun next() = fun next(): Token {
if (currentIndex < tokens.size) tokens[currentIndex++] if (pendingGT > 0) {
pendingGT--
val last = tokens[currentIndex - 1]
return Token(">", last.pos.copy(column = last.pos.column + 1), Token.Type.GT)
}
return if (currentIndex < tokens.size) tokens[currentIndex++]
else Token("", tokens.last().pos, Token.Type.EOF) else Token("", tokens.last().pos, Token.Type.EOF)
}
fun previous() = if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex] fun pushPendingGT() {
pendingGT++
}
fun savePos() = currentIndex fun previous() = if (pendingGT > 0) {
pendingGT-- // This is wrong, previous should go back.
// But we don't really use previous() in generics parser after splitting.
throw IllegalStateException("previous() not supported after pushPendingGT")
} else if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex]
fun savePos() = (currentIndex shl 2) or (pendingGT and 3)
fun restorePos(pos: Int) { fun restorePos(pos: Int) {
currentIndex = pos currentIndex = pos shr 2
pendingGT = pos and 3
} }
fun ensureLabelIsValid(pos: Pos, label: String) { fun ensureLabelIsValid(pos: Pos, label: String) {
@ -106,12 +122,13 @@ class CompilerContext(val tokens: List<Token>) {
errorMessage: String = "expected ${tokenType.name}", errorMessage: String = "expected ${tokenType.name}",
isOptional: Boolean = false isOptional: Boolean = false
): Boolean { ): Boolean {
val pos = savePos()
val t = next() val t = next()
return if (t.type != tokenType) { return if (t.type != tokenType) {
if (!isOptional) { if (!isOptional) {
throw ScriptError(t.pos, errorMessage) throw ScriptError(t.pos, errorMessage)
} else { } else {
previous() restorePos(pos)
false false
} }
} else true } else true
@ -122,20 +139,25 @@ class CompilerContext(val tokens: List<Token>) {
* @return true if token was found and skipped * @return true if token was found and skipped
*/ */
fun skipNextIf(vararg types: Token.Type): Boolean { fun skipNextIf(vararg types: Token.Type): Boolean {
val pos = savePos()
val t = next() val t = next()
return if (t.type in types) return if (t.type in types)
true true
else { else {
previous() restorePos(pos)
false false
} }
} }
@Suppress("unused") @Suppress("unused")
fun skipTokens(vararg tokenTypes: Token.Type) { fun skipTokens(vararg tokenTypes: Token.Type) {
while (next().type in tokenTypes) { /**/ while (hasNext()) {
val pos = savePos()
if (next().type !in tokenTypes) {
restorePos(pos)
break
}
} }
previous()
} }
fun nextNonWhitespace(): Token { fun nextNonWhitespace(): Token {
@ -163,12 +185,13 @@ class CompilerContext(val tokens: List<Token>) {
inline fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean { inline fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
val pos = savePos()
val t = next() val t = next()
return if (t.type == typeId) { return if (t.type == typeId) {
f(t) f(t)
true true
} else { } else {
previous() restorePos(pos)
false false
} }
} }

View File

@ -27,5 +27,6 @@ sealed class TypeDecl(val isNullable:Boolean = false) {
object TypeAny : TypeDecl() object TypeAny : TypeDecl()
object TypeNullableAny : TypeDecl(true) object TypeNullableAny : TypeDecl(true)
class Simple(val name: String,isNullable: Boolean) : TypeDecl(isNullable) class Simple(val name: String, isNullable: Boolean) : TypeDecl(isNullable)
class Generic(val name: String, val args: List<TypeDecl>, isNullable: Boolean) : TypeDecl(isNullable)
} }

View File

@ -236,6 +236,7 @@ class ClassDocsBuilder internal constructor(private val className: String) {
name = name, name = name,
mutable = mutable, mutable = mutable,
type = type?.toMiniTypeRef(), type = type?.toMiniTypeRef(),
initRange = null,
doc = md, doc = md,
nameStart = Pos.builtIn, nameStart = Pos.builtIn,
isStatic = isStatic, isStatic = isStatic,
@ -534,6 +535,9 @@ private fun buildStdlibDocs(): List<MiniDecl> {
) )
// Concurrency helpers // Concurrency helpers
mod.classDoc(name = "Deferred", doc = "Represents a value that will be available in the future.", bases = listOf(type("Obj"))) {
method(name = "await", doc = "Suspend until the value is available and return it.")
}
mod.funDoc( mod.funDoc(
name = "launch", name = "launch",
doc = StdlibInlineDocIndex.topFunDoc("launch") ?: "Launch an asynchronous task and return a `Deferred`.", doc = StdlibInlineDocIndex.topFunDoc("launch") ?: "Launch an asynchronous task and return a `Deferred`.",
@ -551,8 +555,17 @@ private fun buildStdlibDocs(): List<MiniDecl> {
returns = type("lyng.Iterable") returns = type("lyng.Iterable")
) )
// Common types
mod.classDoc(name = "Int", doc = "64-bit signed integer.", bases = listOf(type("Obj")))
mod.classDoc(name = "Real", doc = "64-bit floating point number.", bases = listOf(type("Obj")))
mod.classDoc(name = "Bool", doc = "Boolean value (true or false).", bases = listOf(type("Obj")))
mod.classDoc(name = "Char", doc = "Single character (UTF-16 code unit).", bases = listOf(type("Obj")))
mod.classDoc(name = "Buffer", doc = "Mutable byte array.", bases = listOf(type("Obj")))
mod.classDoc(name = "Regex", doc = "Regular expression.", bases = listOf(type("Obj")))
mod.classDoc(name = "Range", doc = "Arithmetic progression.", bases = listOf(type("Obj")))
// Common Iterable helpers (document top-level extension-like APIs as class members) // Common Iterable helpers (document top-level extension-like APIs as class members)
mod.classDoc(name = "Iterable", doc = StdlibInlineDocIndex.classDoc("Iterable") ?: "Helper operations for iterable collections.") { mod.classDoc(name = "Iterable", doc = StdlibInlineDocIndex.classDoc("Iterable") ?: "Helper operations for iterable collections.", bases = listOf(type("Obj"))) {
fun md(name: String, fallback: String) = StdlibInlineDocIndex.methodDoc("Iterable", name) ?: fallback fun md(name: String, fallback: String) = StdlibInlineDocIndex.methodDoc("Iterable", name) ?: fallback
method(name = "filter", doc = md("filter", "Filter elements by predicate."), params = listOf(ParamDoc("predicate")), returns = type("lyng.Iterable")) method(name = "filter", doc = md("filter", "Filter elements by predicate."), params = listOf(ParamDoc("predicate")), returns = type("lyng.Iterable"))
method(name = "drop", doc = md("drop", "Skip the first N elements."), params = listOf(ParamDoc("n", type("lyng.Int"))), returns = type("lyng.Iterable")) method(name = "drop", doc = md("drop", "Skip the first N elements."), params = listOf(ParamDoc("n", type("lyng.Int"))), returns = type("lyng.Iterable"))
@ -591,7 +604,7 @@ private fun buildStdlibDocs(): List<MiniDecl> {
} }
// Iterator helpers // Iterator helpers
mod.classDoc(name = "Iterator", doc = StdlibInlineDocIndex.classDoc("Iterator") ?: "Iterator protocol for sequential access.") { mod.classDoc(name = "Iterator", doc = StdlibInlineDocIndex.classDoc("Iterator") ?: "Iterator protocol for sequential access.", bases = listOf(type("Obj"))) {
fun md(name: String, fallback: String) = StdlibInlineDocIndex.methodDoc("Iterator", name) ?: fallback fun md(name: String, fallback: String) = StdlibInlineDocIndex.methodDoc("Iterator", name) ?: fallback
method(name = "hasNext", doc = md("hasNext", "Whether another element is available."), returns = type("lyng.Bool")) method(name = "hasNext", doc = md("hasNext", "Whether another element is available."), returns = type("lyng.Bool"))
method(name = "next", doc = md("next", "Return the next element.")) method(name = "next", doc = md("next", "Return the next element."))
@ -600,22 +613,22 @@ private fun buildStdlibDocs(): List<MiniDecl> {
} }
// Exceptions and utilities // Exceptions and utilities
mod.classDoc(name = "Exception", doc = StdlibInlineDocIndex.classDoc("Exception") ?: "Exception helpers.") { mod.classDoc(name = "Exception", doc = StdlibInlineDocIndex.classDoc("Exception") ?: "Exception helpers.", bases = listOf(type("Obj"))) {
method(name = "printStackTrace", doc = StdlibInlineDocIndex.methodDoc("Exception", "printStackTrace") ?: "Print this exception and its stack trace to standard output.") method(name = "printStackTrace", doc = StdlibInlineDocIndex.methodDoc("Exception", "printStackTrace") ?: "Print this exception and its stack trace to standard output.")
} }
mod.classDoc(name = "Enum", doc = StdlibInlineDocIndex.classDoc("Enum") ?: "Base class for all enums.") { mod.classDoc(name = "Enum", doc = StdlibInlineDocIndex.classDoc("Enum") ?: "Base class for all enums.", bases = listOf(type("Obj"))) {
method(name = "name", doc = "Returns the name of this enum constant.", returns = type("lyng.String")) method(name = "name", doc = "Returns the name of this enum constant.", returns = type("lyng.String"))
method(name = "ordinal", doc = "Returns the ordinal of this enum constant.", returns = type("lyng.Int")) method(name = "ordinal", doc = "Returns the ordinal of this enum constant.", returns = type("lyng.Int"))
} }
mod.classDoc(name = "String", doc = StdlibInlineDocIndex.classDoc("String") ?: "String helpers.") { mod.classDoc(name = "String", doc = StdlibInlineDocIndex.classDoc("String") ?: "String helpers.", bases = listOf(type("Obj"))) {
// Only include inline-source method here; Kotlin-embedded methods are now documented via DocHelpers near definitions. // Only include inline-source method here; Kotlin-embedded methods are now documented via DocHelpers near definitions.
method(name = "re", doc = StdlibInlineDocIndex.methodDoc("String", "re") ?: "Compile this string into a regular expression.", returns = type("lyng.Regex")) method(name = "re", doc = StdlibInlineDocIndex.methodDoc("String", "re") ?: "Compile this string into a regular expression.", returns = type("lyng.Regex"))
} }
// StackTraceEntry structure // StackTraceEntry structure
mod.classDoc(name = "StackTraceEntry", doc = StdlibInlineDocIndex.classDoc("StackTraceEntry") ?: "Represents a single stack trace element.") { mod.classDoc(name = "StackTraceEntry", doc = StdlibInlineDocIndex.classDoc("StackTraceEntry") ?: "Represents a single stack trace element.", bases = listOf(type("Obj"))) {
// Fields are not present as declarations in root.lyng's class header docs. Keep seeded defaults. // Fields are not present as declarations in root.lyng's class header docs. Keep seeded defaults.
field(name = "sourceName", doc = "Source (file) name.", type = type("lyng.String")) field(name = "sourceName", doc = "Source (file) name.", type = type("lyng.String"))
field(name = "line", doc = "Line number (1-based).", type = type("lyng.Int")) field(name = "line", doc = "Line number (1-based).", type = type("lyng.Int"))

View File

@ -23,6 +23,7 @@ package net.sergeych.lyng.miniast
import net.sergeych.lyng.Compiler import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Script import net.sergeych.lyng.Script
import net.sergeych.lyng.Source import net.sergeych.lyng.Source
import net.sergeych.lyng.binding.BindingSnapshot
import net.sergeych.lyng.highlight.offsetOf import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.pacman.ImportProvider import net.sergeych.lyng.pacman.ImportProvider
@ -58,7 +59,7 @@ object CompletionEngineLight {
return completeSuspend(text, idx) return completeSuspend(text, idx)
} }
suspend fun completeSuspend(text: String, caret: Int, providedMini: MiniScript? = null): List<CompletionItem> { suspend fun completeSuspend(text: String, caret: Int, providedMini: MiniScript? = null, binding: BindingSnapshot? = null): List<CompletionItem> {
// Ensure stdlib Obj*-defined docs (e.g., String methods) are initialized before registry lookup // Ensure stdlib Obj*-defined docs (e.g., String methods) are initialized before registry lookup
StdlibDocsBootstrap.ensure() StdlibDocsBootstrap.ensure()
val prefix = prefixAt(text, caret) val prefix = prefixAt(text, caret)
@ -73,6 +74,10 @@ object CompletionEngineLight {
val memberDot = DocLookupUtils.findDotLeft(text, word?.first ?: caret) val memberDot = DocLookupUtils.findDotLeft(text, word?.first ?: caret)
if (memberDot != null) { if (memberDot != null) {
// 0) Try chained member call return type inference // 0) Try chained member call return type inference
DocLookupUtils.guessReturnClassFromMemberCallBeforeMini(mini, text, memberDot, imported, binding)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
}
DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDot, imported, mini)?.let { cls -> DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDot, imported, mini)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini) offerMembersAdd(out, prefix, imported, cls, mini)
return out return out
@ -88,7 +93,7 @@ object CompletionEngineLight {
return out return out
} }
// 1) Receiver inference fallback // 1) Receiver inference fallback
(DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDot, imported) ?: DocLookupUtils.guessReceiverClass(text, memberDot, imported, mini))?.let { cls -> (DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDot, imported, binding) ?: DocLookupUtils.guessReceiverClass(text, memberDot, imported, mini))?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini) offerMembersAdd(out, prefix, imported, cls, mini)
return out return out
} }
@ -97,11 +102,16 @@ object CompletionEngineLight {
} }
// Global identifiers: params > local decls > imported > stdlib; Functions > Classes > Values; alphabetical // Global identifiers: params > local decls > imported > stdlib; Functions > Classes > Values; alphabetical
if (mini != null) { offerParamsInScope(out, prefix, mini, text, caret)
offerParamsInScope(out, prefix, mini, text, caret)
val locals = DocLookupUtils.extractLocalsAt(text, caret)
for (name in locals) {
if (name.startsWith(prefix, true)) {
out.add(CompletionItem(name, Kind.Value))
}
} }
val decls = mini?.declarations ?: emptyList() val decls = mini.declarations
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() } val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
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() }
@ -274,33 +284,38 @@ object CompletionEngineLight {
emitGroup(directMap) emitGroup(directMap)
emitGroup(inheritedMap) emitGroup(inheritedMap)
// Supplement with stdlib extension members defined in root.lyng (e.g., fun String.re(...)) // Supplement with extension members (both stdlib and local)
run { run {
val already = (directMap.keys + inheritedMap.keys).toMutableSet() val already = (directMap.keys + inheritedMap.keys).toMutableSet()
val ext = BuiltinDocRegistry.extensionMemberNamesFor(className) val extensions = DocLookupUtils.collectExtensionMemberNames(imported, className, mini)
for (name in ext) { for (name in extensions) {
if (already.contains(name)) continue if (already.contains(name)) continue
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name) val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name, mini)
if (resolved != null) { if (resolved != null) {
when (val member = resolved.second) { val m = resolved.second
val ci = when (m) {
is MiniMemberFunDecl -> { is MiniMemberFunDecl -> {
val params = member.params.joinToString(", ") { it.name } val params = m.params.joinToString(", ") { it.name }
val ci = CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(member.returnType)) CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType))
if (ci.name.startsWith(prefix, true)) out += ci
already.add(name)
} }
is MiniMemberValDecl -> { is MiniFunDecl -> {
val ci = CompletionItem(name, Kind.Field, typeText = typeOf(member.type)) val params = m.params.joinToString(", ") { it.name }
if (ci.name.startsWith(prefix, true)) out += ci CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType))
already.add(name)
} }
is MiniInitDecl -> {} is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type))
is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type))
else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null)
}
if (ci.name.startsWith(prefix, true)) {
out += ci
already.add(name)
} }
} else { } else {
// Fallback: emit simple method name without detailed types
val ci = CompletionItem(name, Kind.Method, tailText = "()", typeText = null) val ci = CompletionItem(name, Kind.Method, tailText = "()", typeText = null)
if (ci.name.startsWith(prefix, true)) out += ci if (ci.name.startsWith(prefix, true)) {
already.add(name) out += ci
already.add(name)
}
} }
} }
} }

View File

@ -91,14 +91,15 @@ object DocLookupUtils {
return null return null
} }
fun findTypeByRange(mini: MiniScript?, name: String, startOffset: Int): MiniTypeRef? { fun findTypeByRange(mini: MiniScript?, name: String, startOffset: Int, text: String? = null, imported: List<String>? = null): MiniTypeRef? {
if (mini == null) return null if (mini == null) return null
val src = mini.range.start.source val src = mini.range.start.source
fun matches(p: net.sergeych.lyng.Pos, len: Int) = src.offsetOf(p).let { s -> startOffset >= s && startOffset < s + len }
for (d in mini.declarations) { for (d in mini.declarations) {
if (d.name == name && src.offsetOf(d.nameStart) == startOffset) { if (d.name == name && matches(d.nameStart, d.name.length)) {
return when (d) { return when (d) {
is MiniValDecl -> d.type is MiniValDecl -> d.type ?: if (text != null && imported != null) inferTypeRefForVal(d, text, imported, mini) else null
is MiniFunDecl -> d.returnType is MiniFunDecl -> d.returnType
else -> null else -> null
} }
@ -106,25 +107,27 @@ object DocLookupUtils {
if (d is MiniFunDecl) { if (d is MiniFunDecl) {
for (p in d.params) { for (p in d.params) {
if (p.name == name && src.offsetOf(p.nameStart) == startOffset) return p.type if (p.name == name && matches(p.nameStart, p.name.length)) return p.type
} }
} }
if (d is MiniClassDecl) { if (d is MiniClassDecl) {
for (m in d.members) { for (m in d.members) {
if (m.name == name && src.offsetOf(m.nameStart) == startOffset) { if (m.name == name && matches(m.nameStart, m.name.length)) {
return when (m) { return when (m) {
is MiniMemberFunDecl -> m.returnType is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type is MiniMemberValDecl -> m.type ?: if (text != null && imported != null) {
inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini)
} else null
else -> null else -> null
} }
} }
} }
for (cf in d.ctorFields) { for (cf in d.ctorFields) {
if (cf.name == name && src.offsetOf(cf.nameStart) == startOffset) return cf.type if (cf.name == name && matches(cf.nameStart, cf.name.length)) return cf.type
} }
for (cf in d.classFields) { for (cf in d.classFields) {
if (cf.name == name && src.offsetOf(cf.nameStart) == startOffset) return cf.type if (cf.name == name && matches(cf.nameStart, cf.name.length)) return cf.type
} }
} }
} }
@ -157,6 +160,21 @@ object DocLookupUtils {
return result.toList() return result.toList()
} }
fun extractLocalsAt(text: String, offset: Int): Set<String> {
val res = mutableSetOf<String>()
// 1) find val/var declarations
val re = Regex("(?:^|[\\n;])\\s*(?:val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)")
re.findAll(text).forEach { m ->
if (m.range.first < offset) res.add(m.groupValues[1])
}
// 2) find implicit assignments
val re2 = Regex("(?:^|[\\n;])\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*=[^=]")
re2.findAll(text).forEach { m ->
if (m.range.first < offset) res.add(m.groupValues[1])
}
return res
}
fun extractImportsFromText(text: String): List<String> { fun extractImportsFromText(text: String): List<String> {
val result = LinkedHashSet<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) val re = Regex("^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", RegexOption.MULTILINE)
@ -232,24 +250,65 @@ object DocLookupUtils {
for ((name, list) in buckets) { for ((name, list) in buckets) {
result[name] = mergeClassDecls(name, list) result[name] = mergeClassDecls(name, list)
} }
// Root object alias
if (result.containsKey("Obj") && !result.containsKey("Any")) {
result["Any"] = result["Obj"]!!
}
return result return result
} }
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? { fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String, localMini: MiniScript? = null): Pair<String, MiniNamedDecl>? {
val classes = aggregateClasses(importedModules, localMini) val classes = aggregateClasses(importedModules, localMini)
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniMemberDecl>? { fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniNamedDecl>? {
val cls = classes[name] ?: return null
cls.members.firstOrNull { it.name == member }?.let { return name to it }
if (!visited.add(name)) return null if (!visited.add(name)) return null
for (baseName in cls.bases) { val cls = classes[name]
dfs(baseName, visited)?.let { return it } if (cls != null) {
cls.members.firstOrNull { it.name == member }?.let { return name to it }
for (baseName in cls.bases) {
dfs(baseName, visited)?.let { return it }
}
} }
// Check for local extensions in this class or bases
localMini?.declarations?.firstOrNull { d ->
(d is MiniFunDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name && d.name == member) ||
(d is MiniValDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name && d.name == member)
}?.let { return name to it }
return null return null
} }
return dfs(className, mutableSetOf()) return dfs(className, mutableSetOf())
} }
fun findMemberAcrossClasses(importedModules: List<String>, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? { fun collectExtensionMemberNames(importedModules: List<String>, className: String, localMini: MiniScript? = null): Set<String> {
val classes = aggregateClasses(importedModules, localMini)
val visited = mutableSetOf<String>()
val result = mutableSetOf<String>()
fun dfs(name: String) {
if (!visited.add(name)) return
// 1) stdlib extensions from BuiltinDocRegistry
result.addAll(BuiltinDocRegistry.extensionMemberNamesFor(name))
// 2) local extensions from mini
localMini?.declarations?.forEach { d ->
if (d is MiniFunDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name) result.add(d.name)
if (d is MiniValDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name) result.add(d.name)
}
// 3) bases
classes[name]?.bases?.forEach { dfs(it) }
}
dfs(className)
// Hardcoded supplements for common containers if not explicitly in bases
if (className == "List" || className == "Array") {
dfs("Collection")
dfs("Iterable")
}
dfs("Any")
dfs("Obj")
return result
}
fun findMemberAcrossClasses(importedModules: List<String>, member: String, localMini: MiniScript? = null): Pair<String, MiniNamedDecl>? {
val classes = aggregateClasses(importedModules, localMini) val classes = aggregateClasses(importedModules, localMini)
// Preferred order for ambiguous common ops // Preferred order for ambiguous common ops
val preference = listOf("Iterable", "Iterator", "List") val preference = listOf("Iterable", "Iterator", "List")
@ -301,6 +360,12 @@ object DocLookupUtils {
if (mini == null) return null if (mini == null) return null
val i = prevNonWs(text, dotPos - 1) val i = prevNonWs(text, dotPos - 1)
if (i < 0) return null if (i < 0) return null
// Handle indexing x[0]. or literal [1].
if (text[i] == ']') {
return guessReceiverClass(text, dotPos, imported, mini)
}
val wordRange = wordRangeAt(text, i + 1) ?: return null val wordRange = wordRangeAt(text, i + 1) ?: return null
val ident = text.substring(wordRange.first, wordRange.second) val ident = text.substring(wordRange.first, wordRange.second)
@ -310,14 +375,14 @@ object DocLookupUtils {
if (ref != null) { if (ref != null) {
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId } val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
if (sym != null) { if (sym != null) {
val type = findTypeByRange(mini, sym.name, sym.declStart) val type = findTypeByRange(mini, sym.name, sym.declStart, text, imported)
simpleClassNameOf(type)?.let { return it } simpleClassNameOf(type)?.let { return it }
} }
} else { } else {
// Check if it's a declaration (e.g. static access to a class) // 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 } val sym = binding.symbols.firstOrNull { it.declStart == wordRange.first && it.name == ident }
if (sym != null) { if (sym != null) {
val type = findTypeByRange(mini, sym.name, sym.declStart) 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) return sym.name
@ -325,13 +390,17 @@ object DocLookupUtils {
} }
} }
// 1) Global declarations in current file (val/var/fun/class/enum) // 1) Declarations in current file (val/var/fun/class/enum), prioritized by proximity
val d = mini.declarations.firstOrNull { it.name == ident } val src = mini.range.start.source
val d = mini.declarations
.filter { it.name == ident && src.offsetOf(it.nameStart) < dotPos }
.maxByOrNull { src.offsetOf(it.nameStart) }
if (d != null) { if (d != null) {
return when (d) { return when (d) {
is MiniClassDecl -> d.name is MiniClassDecl -> d.name
is MiniEnumDecl -> d.name is MiniEnumDecl -> d.name
is MiniValDecl -> simpleClassNameOf(d.type) is MiniValDecl -> simpleClassNameOf(d.type ?: inferTypeRefForVal(d, text, imported, mini))
is MiniFunDecl -> simpleClassNameOf(d.returnType) is MiniFunDecl -> simpleClassNameOf(d.returnType)
} }
} }
@ -343,6 +412,9 @@ object DocLookupUtils {
} }
} }
// 2a) Try to find plain assignment in text if not found in declarations: x = test()
inferTypeFromAssignmentInText(ident, text, imported, mini, beforeOffset = dotPos)?.let { return simpleClassNameOf(it) }
// 3) Recursive chaining: Base.ident. // 3) Recursive chaining: Base.ident.
val dotBefore = findDotLeft(text, wordRange.first) val dotBefore = findDotLeft(text, wordRange.first)
if (dotBefore != null) { if (dotBefore != null) {
@ -353,7 +425,7 @@ object DocLookupUtils {
if (resolved != null) { if (resolved != null) {
val rt = when (val m = resolved.second) { val rt = when (val m = resolved.second) {
is MiniMemberFunDecl -> m.returnType is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini)
else -> null else -> null
} }
return simpleClassNameOf(rt) return simpleClassNameOf(rt)
@ -368,6 +440,43 @@ object DocLookupUtils {
return null return null
} }
private fun inferTypeFromAssignmentInText(ident: String, text: String, imported: List<String>, mini: MiniScript?, beforeOffset: Int = Int.MAX_VALUE): MiniTypeRef? {
// Heuristic: search for "val ident =" or "ident =" in text
val re = Regex("(?:^|[\\n;])\\s*(?:val|var)?\\s*${ident}\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?\\s*(?:=|by)\\s*([^\\n;]+)")
val match = re.findAll(text)
.filter { it.range.first < beforeOffset }
.lastOrNull() ?: return null
val explicitType = match.groupValues.getOrNull(1)?.takeIf { it.isNotBlank() }
if (explicitType != null) return syntheticTypeRef(explicitType)
val expr = match.groupValues.getOrNull(2)?.let { stripComments(it) } ?: return null
return inferTypeRefFromExpression(expr, imported, mini, contextText = text, beforeOffset = beforeOffset)
}
private fun stripComments(text: String): String {
var result = ""
var i = 0
var inString = false
while (i < text.length) {
val ch = text[i]
if (ch == '"' && (i == 0 || text[i - 1] != '\\')) {
inString = !inString
}
if (!inString && ch == '/' && i + 1 < text.length) {
if (text[i + 1] == '/') break // single line comment
if (text[i + 1] == '*') {
// Skip block comment
i += 2
while (i + 1 < text.length && !(text[i] == '*' && text[i + 1] == '/')) i++
i += 2 // Skip '*/'
continue
}
}
result += ch
i++
}
return result.trim()
}
fun guessReturnClassFromMemberCallBeforeMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>, binding: BindingSnapshot? = null): String? { fun guessReturnClassFromMemberCallBeforeMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>, binding: BindingSnapshot? = null): String? {
if (mini == null) return null if (mini == null) return null
var i = prevNonWs(text, dotPos - 1) var i = prevNonWs(text, dotPos - 1)
@ -420,7 +529,9 @@ 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 MiniInitDecl -> null is MiniFunDecl -> m.returnType
is MiniValDecl -> m.type
else -> null
} }
simpleClassNameOf(rt) simpleClassNameOf(rt)
} }
@ -455,13 +566,25 @@ object DocLookupUtils {
return map return map
} }
fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? { fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null, beforeOffset: Int = dotPos): String? {
guessClassFromCallBefore(text, dotPos, imported, mini)?.let { return it } guessClassFromCallBefore(text, dotPos, imported, mini)?.let { return it }
var i = prevNonWs(text, dotPos - 1) var i = prevNonWs(text, dotPos - 1)
if (i >= 0) { if (i >= 0) {
when (text[i]) { when (text[i]) {
'"' -> return "String" '"' -> return "String"
']' -> return "List" ']' -> {
// Check if literal or indexing
val matchingOpen = findMatchingOpenBracket(text, i)
if (matchingOpen != null && matchingOpen > 0) {
val beforeOpen = prevNonWs(text, matchingOpen - 1)
if (beforeOpen >= 0 && (isIdentChar(text[beforeOpen]) || text[beforeOpen] == ')' || text[beforeOpen] == ']')) {
// Likely indexing: infer type of full expression
val exprText = text.substring(0, i + 1)
return simpleClassNameOf(inferTypeRefFromExpression(exprText, imported, mini, beforeOffset = beforeOffset))
}
}
return "List"
}
'}' -> return "Dict" '}' -> return "Dict"
')' -> { ')' -> {
// Parenthesized expression: walk back to matching '(' and inspect the inner expression // Parenthesized expression: walk back to matching '(' and inspect the inner expression
@ -564,7 +687,9 @@ object DocLookupUtils {
val ret = when (member) { val ret = when (member) {
is MiniMemberFunDecl -> member.returnType is MiniMemberFunDecl -> member.returnType
is MiniMemberValDecl -> member.type is MiniMemberValDecl -> member.type
is MiniInitDecl -> null is MiniFunDecl -> member.returnType
is MiniValDecl -> member.type
else -> null
} }
return simpleClassNameOf(ret) return simpleClassNameOf(ret)
} }
@ -627,11 +752,216 @@ object DocLookupUtils {
val ret = when (member) { val ret = when (member) {
is MiniMemberFunDecl -> member.returnType is MiniMemberFunDecl -> member.returnType
is MiniMemberValDecl -> member.type is MiniMemberValDecl -> member.type
is MiniInitDecl -> null is MiniFunDecl -> member.returnType
is MiniValDecl -> member.type
else -> null
} }
return simpleClassNameOf(ret) return simpleClassNameOf(ret)
} }
fun inferTypeRefFromExpression(text: String, imported: List<String>, mini: MiniScript? = null, contextText: String? = null, beforeOffset: Int = Int.MAX_VALUE): MiniTypeRef? {
val trimmed = stripComments(text)
if (trimmed.isEmpty()) return null
val fullText = contextText ?: text
// 1) Literals
if (trimmed.startsWith("\"")) return syntheticTypeRef("String")
if (trimmed.startsWith("[")) return syntheticTypeRef("List")
if (trimmed.startsWith("{")) return syntheticTypeRef("Dict")
if (trimmed == "true" || trimmed == "false") return syntheticTypeRef("Boolean")
if (trimmed.all { it.isDigit() || it == '.' || it == '_' || it == 'e' || it == 'E' }) {
val hasDigits = trimmed.any { it.isDigit() }
if (hasDigits)
return if (trimmed.contains('.') || trimmed.contains('e', ignoreCase = true)) syntheticTypeRef("Real") else syntheticTypeRef("Int")
}
// 2) Function/Constructor calls or Indexing
if (trimmed.endsWith(")")) {
val openParen = findMatchingOpenParen(trimmed, trimmed.length - 1)
if (openParen != null && openParen > 0) {
var j = openParen - 1
while (j >= 0 && trimmed[j].isWhitespace()) j--
val end = j + 1
while (j >= 0 && isIdentChar(trimmed[j])) j--
val start = j + 1
if (start < end) {
val callee = trimmed.substring(start, end)
// Check if it's a member call (dot before callee)
var k = start - 1
while (k >= 0 && trimmed[k].isWhitespace()) k--
if (k >= 0 && trimmed[k] == '.') {
val prevDot = k
// Recursive: try to infer type of what's before the dot
val receiverText = trimmed.substring(0, prevDot)
val receiverType = inferTypeRefFromExpression(receiverText, imported, mini, contextText = fullText, beforeOffset = beforeOffset)
val receiverClass = simpleClassNameOf(receiverType)
if (receiverClass != null) {
val resolved = resolveMemberWithInheritance(imported, receiverClass, callee, mini)
if (resolved != null) {
return when (val m = resolved.second) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, fullText, imported, mini)
else -> null
}
}
}
} else {
// Top-level call or constructor
val classes = aggregateClasses(imported, mini)
if (classes.containsKey(callee)) return syntheticTypeRef(callee)
for (mod in imported) {
val decls = BuiltinDocRegistry.docsForModule(mod)
val fn = decls.asSequence().filterIsInstance<MiniFunDecl>().firstOrNull { it.name == callee }
if (fn != null) return fn.returnType
}
mini?.declarations?.filterIsInstance<MiniFunDecl>()?.firstOrNull { it.name == callee }?.let { return it.returnType }
}
}
}
}
if (trimmed.endsWith("]")) {
val openBracket = findMatchingOpenBracket(trimmed, trimmed.length - 1)
if (openBracket != null && openBracket > 0) {
val receiverText = trimmed.substring(0, openBracket).trim()
if (receiverText.isNotEmpty()) {
val receiverType = inferTypeRefFromExpression(receiverText, imported, mini, contextText = fullText, beforeOffset = beforeOffset)
if (receiverType is MiniGenericType) {
val baseName = simpleClassNameOf(receiverType.base)
if (baseName == "List" && receiverType.args.isNotEmpty()) {
return receiverType.args[0]
}
if (baseName == "Map" && receiverType.args.size >= 2) {
return receiverType.args[1]
}
}
// Fallback for non-generic collections or if base name matches
val baseName = simpleClassNameOf(receiverType)
if (baseName == "List" || baseName == "Array" || baseName == "String") {
if (baseName == "String") return syntheticTypeRef("Char")
return syntheticTypeRef("Any")
}
}
}
}
// 3) Member field or simple identifier at the end
val lastWord = wordRangeAt(trimmed, trimmed.length)
if (lastWord != null && lastWord.second == trimmed.length) {
val ident = trimmed.substring(lastWord.first, lastWord.second)
var k = lastWord.first - 1
while (k >= 0 && trimmed[k].isWhitespace()) k--
if (k >= 0 && trimmed[k] == '.') {
// Member field: receiver.ident
val receiverText = trimmed.substring(0, k).trim()
val receiverType = inferTypeRefFromExpression(receiverText, imported, mini, contextText = fullText, beforeOffset = beforeOffset)
val receiverClass = simpleClassNameOf(receiverType)
if (receiverClass != null) {
val resolved = resolveMemberWithInheritance(imported, receiverClass, ident, mini)
if (resolved != null) {
return when (val m = resolved.second) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, fullText, imported, mini)
else -> null
}
}
}
} else {
// Simple identifier
// 1) Declarations in current file (val/var/fun/class/enum), prioritized by proximity
val src = mini?.range?.start?.source
val d = if (src != null) {
mini.declarations
.filter { it.name == ident && src.offsetOf(it.nameStart) < beforeOffset }
.maxByOrNull { src.offsetOf(it.nameStart) }
} else {
mini?.declarations?.firstOrNull { it.name == ident }
}
if (d != null) {
return when (d) {
is MiniClassDecl -> syntheticTypeRef(d.name)
is MiniEnumDecl -> syntheticTypeRef(d.name)
is MiniValDecl -> d.type ?: inferTypeRefForVal(d, fullText, imported, mini)
is MiniFunDecl -> d.returnType
}
}
// 2) Parameters in any function
for (fd in mini?.declarations?.filterIsInstance<MiniFunDecl>() ?: emptyList()) {
for (p in fd.params) {
if (p.name == ident) return p.type
}
}
// 3) Try to find plain assignment in text: ident = expr
inferTypeFromAssignmentInText(ident, fullText, imported, mini, beforeOffset = beforeOffset)?.let { return it }
// 4) Check if it's a known class (static access)
val classes = aggregateClasses(imported, mini)
if (classes.containsKey(ident)) return syntheticTypeRef(ident)
}
}
return null
}
private fun findMatchingOpenBracket(text: String, closeBracketPos: Int): Int? {
if (closeBracketPos < 0 || closeBracketPos >= text.length || text[closeBracketPos] != ']') return null
var depth = 0
var i = closeBracketPos - 1
while (i >= 0) {
when (text[i]) {
']' -> depth++
'[' -> if (depth == 0) return i else depth--
}
i--
}
return null
}
private fun findMatchingOpenParen(text: String, closeParenPos: Int): Int? {
if (closeParenPos < 0 || closeParenPos >= text.length || text[closeParenPos] != ')') return null
var depth = 0
var i = closeParenPos - 1
while (i >= 0) {
when (text[i]) {
')' -> depth++
'(' -> if (depth == 0) return i else depth--
}
i--
}
return null
}
private fun syntheticTypeRef(name: String): MiniTypeRef =
MiniTypeName(MiniRange(net.sergeych.lyng.Pos.builtIn, net.sergeych.lyng.Pos.builtIn),
listOf(MiniTypeName.Segment(name, MiniRange(net.sergeych.lyng.Pos.builtIn, net.sergeych.lyng.Pos.builtIn))), false)
fun inferTypeRefForVal(vd: MiniValDecl, text: String, imported: List<String>, mini: MiniScript?): MiniTypeRef? {
return inferTypeRefFromInitRange(vd.initRange, vd.nameStart, text, imported, mini)
}
fun inferTypeRefFromInitRange(initRange: MiniRange?, nameStart: net.sergeych.lyng.Pos, text: String, imported: List<String>, mini: MiniScript?): MiniTypeRef? {
val range = initRange ?: return null
val src = mini?.range?.start?.source ?: return null
val start = src.offsetOf(range.start)
val end = src.offsetOf(range.end)
if (start < 0 || start >= end || end > text.length) return null
var exprText = text.substring(start, end).trim()
if (exprText.startsWith("=")) {
exprText = exprText.substring(1).trim()
}
if (exprText.startsWith("by")) {
exprText = exprText.substring(2).trim()
}
val beforeOffset = src.offsetOf(nameStart)
return inferTypeRefFromExpression(exprText, imported, mini, contextText = text, beforeOffset = beforeOffset)
}
fun simpleClassNameOf(t: MiniTypeRef?): String? = when (t) { fun simpleClassNameOf(t: MiniTypeRef?): String? = when (t) {
null -> null null -> null
is MiniTypeName -> t.segments.lastOrNull()?.name is MiniTypeName -> t.segments.lastOrNull()?.name
@ -667,13 +997,13 @@ object DocLookupUtils {
fun enumToSyntheticClass(en: MiniEnumDecl): MiniClassDecl { fun enumToSyntheticClass(en: MiniEnumDecl): MiniClassDecl {
val staticMembers = mutableListOf<MiniMemberDecl>() val staticMembers = mutableListOf<MiniMemberDecl>()
// entries: List // entries: List
staticMembers.add(MiniMemberValDecl(en.range, "entries", false, null, null, en.nameStart, isStatic = true)) staticMembers.add(MiniMemberValDecl(en.range, "entries", false, null, null, null, en.nameStart, isStatic = true))
// valueOf(name: String): Enum // valueOf(name: String): Enum
staticMembers.add(MiniMemberFunDecl(en.range, "valueOf", listOf(MiniParam("name", null, en.nameStart)), null, null, en.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) // Also add each entry as a static member (const)
for (entry in en.entries) { 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)) staticMembers.add(MiniMemberValDecl(en.range, entry, false, MiniTypeName(en.range, listOf(MiniTypeName.Segment(en.name, en.range)), false), null, null, en.nameStart, isStatic = true))
} }
return MiniClassDecl( return MiniClassDecl(

View File

@ -81,7 +81,7 @@ data class MiniTypeVar(
) : MiniTypeRef ) : MiniTypeRef
// Script and declarations (lean subset; can be extended later) // Script and declarations (lean subset; can be extended later)
sealed interface MiniDecl : MiniNode { sealed interface MiniNamedDecl : MiniNode {
val name: String val name: String
val doc: MiniDoc? val doc: MiniDoc?
// Start position of the declaration name identifier in source; end can be derived as start + name.length // Start position of the declaration name identifier in source; end can be derived as start + name.length
@ -89,6 +89,8 @@ sealed interface MiniDecl : MiniNode {
val isExtern: Boolean val isExtern: Boolean
} }
sealed interface MiniDecl : MiniNamedDecl
data class MiniScript( data class MiniScript(
override val range: MiniRange, override val range: MiniRange,
val declarations: MutableList<MiniDecl> = mutableListOf(), val declarations: MutableList<MiniDecl> = mutableListOf(),
@ -172,12 +174,8 @@ data class MiniIdentifier(
) : MiniNode ) : MiniNode
// --- Class member declarations (for built-in/registry docs) --- // --- Class member declarations (for built-in/registry docs) ---
sealed interface MiniMemberDecl : MiniNode { sealed interface MiniMemberDecl : MiniNamedDecl {
val name: String
val doc: MiniDoc?
val nameStart: Pos
val isStatic: Boolean val isStatic: Boolean
val isExtern: Boolean
} }
data class MiniMemberFunDecl( data class MiniMemberFunDecl(
@ -189,6 +187,7 @@ data class MiniMemberFunDecl(
override val nameStart: Pos, override val nameStart: Pos,
override val isStatic: Boolean = false, override val isStatic: Boolean = false,
override val isExtern: Boolean = false, override val isExtern: Boolean = false,
val body: MiniBlock? = null
) : MiniMemberDecl ) : MiniMemberDecl
data class MiniMemberValDecl( data class MiniMemberValDecl(
@ -196,6 +195,7 @@ data class MiniMemberValDecl(
override val name: String, override val name: String,
val mutable: Boolean, val mutable: Boolean,
val type: MiniTypeRef?, val type: MiniTypeRef?,
val initRange: MiniRange?,
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
override val isStatic: Boolean = false, override val isStatic: Boolean = false,
@ -222,6 +222,9 @@ interface MiniAstSink {
fun onEnterClass(node: MiniClassDecl) {} fun onEnterClass(node: MiniClassDecl) {}
fun onExitClass(end: Pos) {} fun onExitClass(end: Pos) {}
fun onEnterFunction(node: MiniFunDecl?) {}
fun onExitFunction(end: Pos) {}
fun onImport(node: MiniImport) {} fun onImport(node: MiniImport) {}
fun onFunDecl(node: MiniFunDecl) {} fun onFunDecl(node: MiniFunDecl) {}
fun onValDecl(node: MiniValDecl) {} fun onValDecl(node: MiniValDecl) {}
@ -254,6 +257,7 @@ class MiniAstBuilder : MiniAstSink {
private val classStack = ArrayDeque<MiniClassDecl>() private val classStack = ArrayDeque<MiniClassDecl>()
private var lastDoc: MiniDoc? = null private var lastDoc: MiniDoc? = null
private var scriptDepth: Int = 0 private var scriptDepth: Int = 0
private var functionDepth: Int = 0
fun build(): MiniScript? = currentScript fun build(): MiniScript? = currentScript
@ -291,6 +295,14 @@ class MiniAstBuilder : MiniAstSink {
} }
} }
override fun onEnterFunction(node: MiniFunDecl?) {
functionDepth++
}
override fun onExitFunction(end: Pos) {
functionDepth--
}
override fun onImport(node: MiniImport) { override fun onImport(node: MiniImport) {
currentScript?.imports?.add(node) currentScript?.imports?.add(node)
} }
@ -298,7 +310,7 @@ class MiniAstBuilder : MiniAstSink {
override fun onFunDecl(node: MiniFunDecl) { override fun onFunDecl(node: MiniFunDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc) val attach = node.copy(doc = node.doc ?: lastDoc)
val currentClass = classStack.lastOrNull() val currentClass = classStack.lastOrNull()
if (currentClass != null) { if (currentClass != null && functionDepth == 0) {
// Convert MiniFunDecl to MiniMemberFunDecl for inclusion in members // Convert MiniFunDecl to MiniMemberFunDecl for inclusion in members
val member = MiniMemberFunDecl( val member = MiniMemberFunDecl(
range = attach.range, range = attach.range,
@ -308,13 +320,29 @@ class MiniAstBuilder : MiniAstSink {
doc = attach.doc, doc = attach.doc,
nameStart = attach.nameStart, nameStart = attach.nameStart,
isStatic = false, // TODO: track static if needed isStatic = false, // TODO: track static if needed
isExtern = attach.isExtern isExtern = attach.isExtern,
body = attach.body
) )
// Need to update the class in the stack since it's immutable-ish (data class) // Need to update the class in the stack since it's immutable-ish (data class)
classStack.removeLast() // Check if we already have this member (from a previous onFunDecl call for the same function)
classStack.addLast(currentClass.copy(members = currentClass.members + member)) val existing = currentClass.members.filterIsInstance<MiniMemberFunDecl>().find { it.name == attach.name && it.nameStart == attach.nameStart }
if (existing != null) {
val members = currentClass.members.map { if (it === existing) member else it }
classStack.removeLast()
classStack.addLast(currentClass.copy(members = members))
} else {
classStack.removeLast()
classStack.addLast(currentClass.copy(members = currentClass.members + member))
}
} else { } else {
currentScript?.declarations?.add(attach) // Check if already in declarations to avoid duplication
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 lastDoc = null
} }
@ -322,21 +350,36 @@ class MiniAstBuilder : MiniAstSink {
override fun onValDecl(node: MiniValDecl) { override fun onValDecl(node: MiniValDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc) val attach = node.copy(doc = node.doc ?: lastDoc)
val currentClass = classStack.lastOrNull() val currentClass = classStack.lastOrNull()
if (currentClass != null) { if (currentClass != null && functionDepth == 0) {
val member = MiniMemberValDecl( val member = MiniMemberValDecl(
range = attach.range, range = attach.range,
name = attach.name, name = attach.name,
mutable = attach.mutable, mutable = attach.mutable,
type = attach.type, type = attach.type,
initRange = attach.initRange,
doc = attach.doc, doc = attach.doc,
nameStart = attach.nameStart, nameStart = attach.nameStart,
isStatic = false, // TODO: track static if needed isStatic = false, // TODO: track static if needed
isExtern = attach.isExtern isExtern = attach.isExtern
) )
classStack.removeLast() // Duplicates for vals are rare but possible if Compiler calls it twice
classStack.addLast(currentClass.copy(members = currentClass.members + member)) val existing = currentClass.members.filterIsInstance<MiniMemberValDecl>().find { it.name == attach.name && it.nameStart == attach.nameStart }
if (existing != null) {
val members = currentClass.members.map { if (it === existing) member else it }
classStack.removeLast()
classStack.addLast(currentClass.copy(members = members))
} else {
classStack.removeLast()
classStack.addLast(currentClass.copy(members = currentClass.members + member))
}
} else { } else {
currentScript?.declarations?.add(attach) 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 lastDoc = null
} }

View File

@ -4,8 +4,6 @@
*/ */
package net.sergeych.lyng.miniast package net.sergeych.lyng.miniast
import net.sergeych.lyng.obj.ObjString
object StdlibDocsBootstrap { object StdlibDocsBootstrap {
// Simple idempotent guard; races are harmless as initializer side-effects are idempotent // Simple idempotent guard; races are harmless as initializer side-effects are idempotent
private var ensured = false private var ensured = false
@ -16,7 +14,25 @@ object StdlibDocsBootstrap {
// Touch core Obj* types whose docs are registered via addFnDoc/addConstDoc // Touch core Obj* types whose docs are registered via addFnDoc/addConstDoc
// Accessing .type forces their static initializers to run and register docs. // Accessing .type forces their static initializers to run and register docs.
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val _string = ObjString.type val _string = net.sergeych.lyng.obj.ObjString.type
@Suppress("UNUSED_VARIABLE")
val _any = net.sergeych.lyng.obj.Obj.rootObjectType
@Suppress("UNUSED_VARIABLE")
val _list = net.sergeych.lyng.obj.ObjList.type
@Suppress("UNUSED_VARIABLE")
val _map = net.sergeych.lyng.obj.ObjMap.type
@Suppress("UNUSED_VARIABLE")
val _int = net.sergeych.lyng.obj.ObjInt.type
@Suppress("UNUSED_VARIABLE")
val _real = net.sergeych.lyng.obj.ObjReal.type
@Suppress("UNUSED_VARIABLE")
val _bool = net.sergeych.lyng.obj.ObjBool.type
@Suppress("UNUSED_VARIABLE")
val _regex = net.sergeych.lyng.obj.ObjRegex.type
@Suppress("UNUSED_VARIABLE")
val _range = net.sergeych.lyng.obj.ObjRange.type
@Suppress("UNUSED_VARIABLE")
val _buffer = net.sergeych.lyng.obj.ObjBuffer.type
} catch (_: Throwable) { } catch (_: Throwable) {
// Best-effort; absence should not break consumers // Best-effort; absence should not break consumers
} finally { } finally {

View File

@ -25,6 +25,9 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import net.sergeych.lyng.* import net.sergeych.lyng.*
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
@ -524,20 +527,46 @@ open class Obj {
companion object { companion object {
val rootObjectType = ObjClass("Obj").apply { val rootObjectType = ObjClass("Obj").apply {
addFn("toString", true) { addFnDoc(
name = "toString",
doc = "Returns a string representation of the object.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisObj.toString(this, true) thisObj.toString(this, true)
} }
addFn("inspect", true) { addFnDoc(
name = "inspect",
doc = "Returns a detailed string representation for debugging.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisObj.inspect(this).toObj() thisObj.inspect(this).toObj()
} }
addFn("contains") { addFnDoc(
name = "contains",
doc = "Returns true if the object contains the given element.",
params = listOf(ParamDoc("element")),
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib"
) {
ObjBool(thisObj.contains(this, args.firstAndOnly())) ObjBool(thisObj.contains(this, args.firstAndOnly()))
} }
// utilities // utilities
addFn("let") { addFnDoc(
name = "let",
doc = "Calls the specified function block with `this` value as its argument and returns its result.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj))) args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
} }
addFn("apply") { addFnDoc(
name = "apply",
doc = "Calls the specified function block with `this` value as its receiver and returns `this` value.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
val body = args.firstAndOnly() val body = args.firstAndOnly()
(thisObj as? ObjInstance)?.let { (thisObj as? ObjInstance)?.let {
body.callOn(ApplyScope(this, it.instanceScope)) body.callOn(ApplyScope(this, it.instanceScope))
@ -546,11 +575,21 @@ open class Obj {
} }
thisObj thisObj
} }
addFn("also") { addFnDoc(
name = "also",
doc = "Calls the specified function block with `this` value as its argument and returns `this` value.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj))) args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
thisObj thisObj
} }
addFn("run") { addFnDoc(
name = "run",
doc = "Calls the specified function block with `this` value as its receiver and returns its result.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
args.firstAndOnly().callOn(this) args.firstAndOnly().callOn(this)
} }
addFn("getAt") { addFn("getAt") {
@ -563,7 +602,12 @@ open class Obj {
thisObj.putAt(this, requiredArg<Obj>(0), newValue) thisObj.putAt(this, requiredArg<Obj>(0), newValue)
newValue newValue
} }
addFn("toJsonString") { addFnDoc(
name = "toJsonString",
doc = "Encodes this object to a JSON string.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisObj.toJson(this).toString().toObj() thisObj.toJson(this).toString().toObj()
} }
} }

View File

@ -20,6 +20,7 @@ package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
@ -178,7 +179,12 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu
else -> scope.raiseIllegalState("illegal type code for Int: $lynonType") else -> scope.raiseIllegalState("illegal type code for Int: $lynonType")
} }
}.apply { }.apply {
addFn("toInt") { addFnDoc(
name = "toInt",
doc = "Returns this integer (identity operation).",
returns = net.sergeych.lyng.miniast.type("lyng.Int"),
moduleName = "lyng.stdlib"
) {
thisObj thisObj
} }
} }

View File

@ -21,6 +21,7 @@
package net.sergeych.lyng package net.sergeych.lyng
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.miniast.* import net.sergeych.lyng.miniast.*
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -274,6 +275,89 @@ class MiniAstTest {
assertEquals("Doc6", e1.doc?.summary) assertEquals("Doc6", e1.doc?.summary)
} }
@Test
fun resolve_inferred_member_type() = runTest {
val code = """
object O3 {
val name = "ozone"
}
val x = O3.name
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()
val type = DocLookupUtils.findTypeByRange(mini, "x", code.indexOf("val x") + 4, code, emptyList())
assertEquals("String", DocLookupUtils.simpleClassNameOf(type))
}
@Test
fun resolve_inferred_val_type_from_extern_fun() = runTest {
val code = """
extern fun test(a: Int): List<Int>
val x = test(1)
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val vd = mini.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
assertNotNull(vd)
val inferred = DocLookupUtils.inferTypeRefForVal(vd, code, emptyList(), mini)
assertNotNull(inferred)
assertTrue(inferred is MiniGenericType)
assertEquals("List", (inferred.base as MiniTypeName).segments.last().name)
val code2 = """
extern fun test2(a: Int): String
val y = test2(1)
""".trimIndent()
val (_, sink2) = compileWithMini(code2)
val mini2 = sink2.build()
val vd2 = mini2?.declarations?.filterIsInstance<MiniValDecl>()?.firstOrNull { it.name == "y" }
assertNotNull(vd2)
val inferred2 = DocLookupUtils.inferTypeRefForVal(vd2, code2, emptyList(), mini2)
assertNotNull(inferred2)
assertTrue(inferred2 is MiniTypeName)
assertEquals("String", inferred2.segments.last().name)
val code3 = """
extern object API {
fun getData(): List<String>
}
val x = API.getData()
""".trimIndent()
val (_, sink3) = compileWithMini(code3)
val mini3 = sink3.build()
val vd3 = mini3?.declarations?.filterIsInstance<MiniValDecl>()?.firstOrNull { it.name == "x" }
assertNotNull(vd3)
val inferred3 = DocLookupUtils.inferTypeRefForVal(vd3, code3, emptyList(), mini3)
assertNotNull(inferred3)
assertTrue(inferred3 is MiniGenericType)
assertEquals("List", (inferred3.base as MiniTypeName).segments.last().name)
}
@Test
fun resolve_inferred_val_type_cross_script() = runTest {
val dCode = "extern fun test(a: Int): List<Int>"
val mainCode = "val x = test(1)"
val (_, dSink) = compileWithMini(dCode)
val dMini = dSink.build()!!
val (_, mainSink) = compileWithMini(mainCode)
val mainMini = mainSink.build()!!
// Merge manually
val merged = mainMini.copy(declarations = (mainMini.declarations + dMini.declarations).toMutableList())
val vd = merged.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
assertNotNull(vd)
val inferred = DocLookupUtils.inferTypeRefForVal(vd, mainCode, emptyList(), merged)
assertNotNull(inferred)
assertTrue(inferred is MiniGenericType)
assertEquals("List", (inferred.base as MiniTypeName).segments.last().name)
}
@Test @Test
fun miniAst_captures_user_sample_extern_doc() = runTest { fun miniAst_captures_user_sample_extern_doc() = runTest {
val code = """ val code = """
@ -311,4 +395,48 @@ class MiniAstTest {
assertEquals("O3", resolved.first) assertEquals("O3", resolved.first)
assertEquals("doc for name", resolved.second.doc?.summary) assertEquals("doc for name", resolved.second.doc?.summary)
} }
@Test
fun miniAst_captures_nested_generics() = runTest {
val code = """
val x: Map<String, List<Int>> = {}
"""
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val vd = mini.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
assertNotNull(vd)
val ty = vd.type as MiniGenericType
assertEquals("Map", (ty.base as MiniTypeName).segments.last().name)
assertEquals(2, ty.args.size)
val arg1 = ty.args[0] as MiniTypeName
assertEquals("String", arg1.segments.last().name)
val arg2 = ty.args[1] as MiniGenericType
assertEquals("List", (arg2.base as MiniTypeName).segments.last().name)
assertEquals(1, arg2.args.size)
val innerArg = arg2.args[0] as MiniTypeName
assertEquals("Int", innerArg.segments.last().name)
}
@Test
fun inferTypeForValWithInference() = runTest {
val code = """
extern fun test(): List<Int>
val x = test()
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val vd = mini.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
assertNotNull(vd)
val imported = listOf("lyng.stdlib")
val src = mini.range.start.source
val type = DocLookupUtils.findTypeByRange(mini, "x", src.offsetOf(vd.nameStart), code, imported)
assertNotNull(type)
val className = DocLookupUtils.simpleClassNameOf(type)
assertEquals("List", className)
}
} }

View File

@ -174,4 +174,308 @@ class CompletionEngineLightTest {
val ns = names(items) val ns = names(items)
assertTrue(ns.contains("myField"), "Class field 'myField' should be proposed, but got: $ns") assertTrue(ns.contains("myField"), "Class field 'myField' should be proposed, but got: $ns")
} }
@Test
fun inferredTypeFromFunctionCall() = runBlocking {
val code = """
extern fun test(a: Int): List<Int>
val x = test(1)
val y = x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for inferred List type, but got: $ns")
}
@Test
fun inferredTypeFromMemberCall() = runBlocking {
val code = """
extern class MyClass {
fun getList(): List<String>
}
extern val c: MyClass
val x = c.getList()
val y = x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for inferred List type from member call, but got: $ns")
}
@Test
fun inferredTypeFromListLiteral() = runBlocking {
val code = """
val x = [1, 2, 3]
val y = x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for inferred List type from literal, but got: $ns")
}
@Test
fun inferredTypeAfterIndexing() = runBlocking {
val code = """
extern fun test(): List<String>
val x = test()
val y = x[0].<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
// Should contain String members, e.g., 'length' or 're'
assertTrue(ns.contains("length"), "String member 'length' should be suggested after indexing List<String>, but got: $ns")
assertTrue(ns.contains("re"), "String member 're' should be suggested after indexing List<String>, but got: $ns")
}
@Test
fun inferredTypeFromAssignmentWithoutVal() = runBlocking {
val code = """
extern fun test(): List<String>
x = test()
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for variable assigned without 'val', but got: $ns")
assertTrue(ns.contains("add"), "List member 'add' should be suggested for variable assigned without 'val', but got: $ns")
}
@Test
fun inferredTypeAfterIndexingWithoutVal() = runBlocking {
val code = """
extern fun test(): List<String>
x = test()
x[0].<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
// String members include 'trim', 'lower', etc.
assertTrue(ns.contains("trim"), "String member 'trim' should be suggested for x[0] where x assigned without val, but got: $ns")
assertFalse(ns.contains("add"), "List member 'add' should NOT be suggested for x[0], but got: $ns")
}
@Test
fun transitiveInferenceWithoutVal() = runBlocking {
val code = """
extern fun test(): List<String>
x = test()
y = x
y.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for transitive inference, but got: $ns")
}
@Test
fun objectMemberReturnInference() = runBlocking {
val code = """
object O {
fun getList(): List<String> = []
}
O.getList().<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for object member call, but got: $ns")
}
@Test
fun directFunctionCallCompletion() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
test(1).<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for direct function call, but got: $ns")
assertTrue(ns.contains("map"), "Inherited member 'map' should be suggested for List, but got: $ns")
}
@Test
fun completionWithTrailingDotError() = runBlocking {
// This simulates typing mid-expression where the script is technically invalid
val code = """
extern fun test(): List<String>
test().<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested even if script ends with a dot, but got: $ns")
}
@Test
fun listLiteralCompletion() = runBlocking {
val code = "[].<caret>"
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for [], but got: $ns")
assertTrue(ns.contains("map"), "Inherited member 'map' should be suggested for [], but got: $ns")
}
@Test
fun userReportedSample() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
x = test(1)
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for x, but got: $ns")
}
@Test
fun userReportedSampleIndexed() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
x = test(1)
x[0].<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "String member 'size' should be suggested for x[0], but got: $ns")
assertTrue(ns.contains("trim"), "String member 'trim' should be suggested for x[0], but got: $ns")
}
@Test
fun userReportedSampleImplicitVariable() = runBlocking {
val code = """
extern fun test(): List<String>
x = test()
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for implicit variable x, but got: $ns")
}
@Test
fun userReportedSampleNoDot() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
x = test(1)
x[0]<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("x"), "Implicit variable 'x' should be suggested as global, but got: $ns")
}
@Test
fun userReportedIssue_X_equals_test2() = runBlocking {
val code = """
extern fun test2(): List<String>
x = test2
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
// Since test2 is a function, x = test2 (without parens) should probably be the function itself,
// but current DocLookupUtils returns returnType.
// If it returns List<String>, then size should be there.
assertTrue(ns.contains("size"), "List member 'size' should be suggested for x = test2, but got: $ns")
}
@Test
fun anyMembersOnInferred() = runBlocking {
val code = """
x = 42
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("toString"), "Any member 'toString' should be suggested for x=42, but got: $ns")
assertTrue(ns.contains("let"), "Any member 'let' should be suggested for x=42, but got: $ns")
}
@Test
fun charMembersOnIndexedString() = runBlocking {
val code = """
x = "hello"
x[0].<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("code"), "Char member 'code' should be suggested for indexed string x[0], but got: $ns")
assertTrue(ns.contains("toString"), "Any member 'toString' should be suggested for x[0], but got: $ns")
}
@Test
fun extensionMemberOnInferredList() = runBlocking {
val code = """
extern fun getNames(): List<String>
ns = getNames()
ns.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("map"), "Extension member 'map' should be suggested for List, but got: $ns")
assertTrue(ns.contains("filter"), "Extension member 'filter' should be suggested for List, but got: $ns")
}
@Test
fun inferredTypeFromExternFunWithVal() = runBlocking {
val code = """
extern fun test(a: Int): List<Int>
val x = test(1)
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for val x = test(1), but got: $ns")
}
@Test
fun userReportedNestedSample() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
class X(fld1, fld2) {
var prop
get() { 12 }
set(value) {
val x = test(2)
x.<caret>
}
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for local val x inside set(), but got: $ns")
}
@Test
fun userReportedNestedSampleIndexed() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
class X(fld1, fld2) {
var prop
get() { 12 }
set(value) {
val x = test(2)
x[0].<caret>
}
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "String member 'size' should be suggested for local x[0] inside set(), but got: $ns")
}
@Test
fun nestedShadowingCompletion() = runBlocking {
val code = """
val x = 42
class X {
fun test() {
val x = "hello"
x.<caret>
}
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
// Should contain String members (like trim)
assertTrue(ns.contains("trim"), "String member 'trim' should be suggested for shadowed x, but got: $ns")
}
} }