plugin with type declarations, collection types and much better type tracking for autocomplete
This commit is contained in:
parent
aba0048a83
commit
fe5dded7af
@ -45,6 +45,8 @@ dependencies {
|
||||
// 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
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2")
|
||||
testImplementation("org.opentest4j:opentest4j:1.3.0")
|
||||
}
|
||||
|
||||
intellij {
|
||||
|
||||
@ -102,7 +102,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
|
||||
// Delegate computation to the shared engine to keep behavior in sync with tests
|
||||
val engineItems = try {
|
||||
runBlocking { CompletionEngineLight.completeSuspend(text, caret, mini) }
|
||||
runBlocking { CompletionEngineLight.completeSuspend(text, caret, mini, binding) }
|
||||
} catch (t: Throwable) {
|
||||
if (DEBUG_COMPLETION) log.warn("[LYNG_DEBUG] Engine completion failed: ${t.message}")
|
||||
emptyList()
|
||||
@ -185,33 +185,51 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
|
||||
?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini)
|
||||
if (!inferredClass.isNullOrBlank()) {
|
||||
val ext = BuiltinDocRegistry.extensionMemberNamesFor(inferredClass)
|
||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${'$'}{ext}")
|
||||
val ext = DocLookupUtils.collectExtensionMemberNames(imported, inferredClass, mini)
|
||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${ext}")
|
||||
for (name in ext) {
|
||||
if (existing.contains(name)) continue
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name, mini)
|
||||
if (resolved != null) {
|
||||
when (val member = resolved.second) {
|
||||
val m = resolved.second
|
||||
val builder = when (m) {
|
||||
is MiniMemberFunDecl -> {
|
||||
val params = member.params.joinToString(", ") { it.name }
|
||||
val ret = typeOf(member.returnType)
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
val params = m.params.joinToString(", ") { it.name }
|
||||
val ret = typeOf(m.returnType)
|
||||
LookupElementBuilder.create(name)
|
||||
.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)
|
||||
.withInsertHandler(ParenInsertHandler)
|
||||
emit(builder)
|
||||
existing.add(name)
|
||||
}
|
||||
is MiniMemberValDecl -> {
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||
.withTypeText(typeOf(member.type), true)
|
||||
emit(builder)
|
||||
existing.add(name)
|
||||
LookupElementBuilder.create(name)
|
||||
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||
.withTypeText(typeOf(m.type), true)
|
||||
}
|
||||
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 {
|
||||
// Fallback: emit simple method name without detailed types
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
@ -455,27 +473,44 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, name, mini)
|
||||
if (resolved != null) {
|
||||
val member = resolved.second
|
||||
when (member) {
|
||||
val builder = when (member) {
|
||||
is MiniMemberFunDecl -> {
|
||||
val params = member.params.joinToString(", ") { it.name }
|
||||
val ret = typeOf(member.returnType)
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
LookupElementBuilder.create(name)
|
||||
.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)
|
||||
.withInsertHandler(ParenInsertHandler)
|
||||
emit(builder)
|
||||
already.add(name)
|
||||
}
|
||||
is MiniMemberValDecl -> {
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(AllIcons.Nodes.Field)
|
||||
LookupElementBuilder.create(name)
|
||||
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||
.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 {
|
||||
// Synthetic fallback: method without detailed params/types to improve UX in absence of docs
|
||||
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
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name, mini)
|
||||
if (resolved != null) {
|
||||
when (val member = resolved.second) {
|
||||
val m = resolved.second
|
||||
val builder = when (m) {
|
||||
is MiniMemberFunDecl -> {
|
||||
val params = member.params.joinToString(", ") { it.name }
|
||||
val ret = typeOf(member.returnType)
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
val params = m.params.joinToString(", ") { it.name }
|
||||
val ret = typeOf(m.returnType)
|
||||
LookupElementBuilder.create(name)
|
||||
.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)
|
||||
.withInsertHandler(ParenInsertHandler)
|
||||
emit(builder)
|
||||
already.add(name)
|
||||
continue
|
||||
}
|
||||
is MiniMemberValDecl -> {
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||
.withTypeText(typeOf(member.type), true)
|
||||
emit(builder)
|
||||
already.add(name)
|
||||
continue
|
||||
LookupElementBuilder.create(name)
|
||||
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||
.withTypeText(typeOf(m.type), true)
|
||||
}
|
||||
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)
|
||||
already.add(name)
|
||||
continue
|
||||
}
|
||||
// Fallback: emit without detailed types if we couldn't resolve
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
|
||||
@ -68,6 +68,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
// 1. Get merged mini-AST from Manager (handles local + .lyng.d merged declarations)
|
||||
val mini = LyngAstManager.getMiniAst(file) ?: return null
|
||||
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
|
||||
// 1) Use unified declaration detection
|
||||
@ -78,7 +79,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
if (d.name == name) {
|
||||
val s: Int = miniSource.offsetOf(d.nameStart)
|
||||
if (s <= offset && s + d.name.length > offset) {
|
||||
return renderDeclDoc(d)
|
||||
return renderDeclDoc(d, text, mini, imported)
|
||||
}
|
||||
}
|
||||
// Handle members if it was a member
|
||||
@ -105,6 +106,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
initRange = null,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
@ -122,6 +124,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
initRange = null,
|
||||
doc = null,
|
||||
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
|
||||
mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
|
||||
@ -209,6 +212,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
initRange = null,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
@ -226,6 +230,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
initRange = null,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
@ -308,6 +313,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
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}")
|
||||
@ -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
|
||||
mini.declarations.firstOrNull { it.name == ident }?.let {
|
||||
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)
|
||||
@ -338,13 +347,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
if (arity != null && chosen.params.size != arity && matches.size > 1) {
|
||||
return renderOverloads(ident, matches)
|
||||
}
|
||||
return renderDeclDoc(chosen)
|
||||
return renderDeclDoc(chosen, text, mini, imported)
|
||||
}
|
||||
// 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
|
||||
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
||||
docs.filterIsInstance<MiniEnumDecl>().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, text, mini, imported) }
|
||||
}
|
||||
// Defensive fallback: if nothing found and it's a well-known stdlib function, render minimal inline docs
|
||||
if (ident == "println" || ident == "print") {
|
||||
@ -364,6 +373,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
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 {
|
||||
@ -383,6 +396,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
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 {
|
||||
@ -396,6 +413,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
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 MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
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)
|
||||
}
|
||||
|
||||
private fun renderDeclDoc(d: MiniDecl): String {
|
||||
private fun renderDeclDoc(d: MiniDecl, text: String, mini: MiniScript, imported: List<String>): String {
|
||||
val title = when (d) {
|
||||
is MiniFunDecl -> "function ${d.name}${signatureOf(d)}"
|
||||
is MiniClassDecl -> "class ${d.name}"
|
||||
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
|
||||
val raw = d.doc?.raw
|
||||
@ -506,7 +535,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
|
||||
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 staticStr = if (m.isStatic) "static " else ""
|
||||
val title = "${staticStr}${kind} $className.${m.name}${ts}"
|
||||
@ -527,7 +556,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
is MiniFunctionType -> ": (..) -> ..${if (t.nullable) "?" else ""}"
|
||||
is MiniTypeVar -> ": ${t.name}${if (t.nullable) "?" else ""}"
|
||||
null -> ""
|
||||
null -> ": Object?"
|
||||
}
|
||||
|
||||
private fun signatureOf(fn: MiniFunDecl): String {
|
||||
|
||||
@ -63,6 +63,10 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
is MiniMemberFunDecl -> "Function"
|
||||
is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value"
|
||||
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)))
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ actual object ArgBuilderProvider {
|
||||
private val tl = object : ThreadLocal<AndroidArgsBuilder>() {
|
||||
override fun initialValue(): AndroidArgsBuilder = AndroidArgsBuilder()
|
||||
}
|
||||
actual fun acquire(): ArgsBuilder = tl.get()
|
||||
actual fun acquire(): ArgsBuilder = tl.get()!!
|
||||
}
|
||||
|
||||
private class AndroidArgsBuilder : ArgsBuilder {
|
||||
|
||||
@ -973,7 +973,10 @@ class Compiler(
|
||||
private fun parseTypeDeclarationWithMini(): Pair<TypeDecl, MiniTypeRef?> {
|
||||
// Only parse a type if a ':' follows; otherwise keep current behavior
|
||||
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)*
|
||||
val segments = mutableListOf<MiniTypeName.Segment>()
|
||||
var first = true
|
||||
@ -1009,41 +1012,28 @@ class Compiler(
|
||||
else MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable)
|
||||
}
|
||||
|
||||
// Optional generic arguments: '<' Type (',' Type)* '>' — single-level only (no nested generics for now)
|
||||
var args: MutableList<MiniTypeRef>? = null
|
||||
// Optional generic arguments: '<' Type (',' Type)* '>'
|
||||
var miniArgs: MutableList<MiniTypeRef>? = null
|
||||
var semArgs: MutableList<TypeDecl>? = null
|
||||
val afterBasePos = cc.savePos()
|
||||
if (cc.skipTokenOfType(Token.Type.LT, isOptional = true)) {
|
||||
args = mutableListOf()
|
||||
miniArgs = mutableListOf()
|
||||
semArgs = mutableListOf()
|
||||
do {
|
||||
// Parse argument as simple or qualified type (single level), with optional nullable '?'
|
||||
val argSegs = mutableListOf<MiniTypeName.Segment>()
|
||||
var argFirst = true
|
||||
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 (argSem, argMini) = parseTypeExpressionWithMini()
|
||||
miniArgs += argMini
|
||||
semArgs += argSem
|
||||
|
||||
val sep = cc.next()
|
||||
when (sep.type) {
|
||||
Token.Type.COMMA -> { /* continue */
|
||||
}
|
||||
|
||||
Token.Type.GT -> break
|
||||
else -> sep.raiseSyntax("expected ',' or '>' in generic arguments")
|
||||
if (sep.type == Token.Type.COMMA) {
|
||||
// continue
|
||||
} else if (sep.type == Token.Type.GT) {
|
||||
break
|
||||
} else if (sep.type == Token.Type.SHR) {
|
||||
cc.pushPendingGT()
|
||||
break
|
||||
} else {
|
||||
sep.raiseSyntax("expected ',' or '>' in generic arguments")
|
||||
}
|
||||
} while (true)
|
||||
lastEnd = cc.currentPos()
|
||||
@ -1055,10 +1045,11 @@ class Compiler(
|
||||
val isNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
@ -1474,7 +1465,9 @@ class Compiler(
|
||||
|
||||
"init" -> {
|
||||
if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||
miniSink?.onEnterFunction(null)
|
||||
val block = parseBlock()
|
||||
miniSink?.onExitFunction(cc.currentPos())
|
||||
lastParsedBlockRange?.let { range ->
|
||||
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
|
||||
}
|
||||
@ -2714,8 +2707,7 @@ class Compiler(
|
||||
val declDocLocal = pendingDeclDoc
|
||||
val outerLabel = lastLabel
|
||||
|
||||
// Emit MiniFunDecl before body parsing (body range unknown yet)
|
||||
run {
|
||||
val node = run {
|
||||
val params = argsDeclaration.params.map { p ->
|
||||
MiniParam(
|
||||
name = p.name,
|
||||
@ -2737,8 +2729,10 @@ class Compiler(
|
||||
)
|
||||
miniSink?.onFunDecl(node)
|
||||
pendingDeclDoc = null
|
||||
node
|
||||
}
|
||||
|
||||
miniSink?.onEnterFunction(node)
|
||||
return inCodeContext(CodeContext.Function(name)) {
|
||||
cc.labels.add(name)
|
||||
outerLabel?.let { cc.labels.add(it) }
|
||||
@ -2941,6 +2935,7 @@ class Compiler(
|
||||
isExtern = actualExtern
|
||||
)
|
||||
miniSink?.onFunDecl(node)
|
||||
miniSink?.onExitFunction(cc.currentPos())
|
||||
}
|
||||
}
|
||||
|
||||
@ -3186,9 +3181,11 @@ class Compiler(
|
||||
while (true) {
|
||||
val t = cc.skipWsTokens()
|
||||
if (t.isId("get")) {
|
||||
val getStart = cc.currentPos()
|
||||
cc.next() // consume 'get'
|
||||
cc.requireToken(Token.Type.LPAREN)
|
||||
cc.requireToken(Token.Type.RPAREN)
|
||||
miniSink?.onEnterFunction(null)
|
||||
getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||
cc.skipWsTokens()
|
||||
parseBlock()
|
||||
@ -3200,11 +3197,14 @@ class Compiler(
|
||||
} else {
|
||||
throw ScriptError(cc.current().pos, "Expected { or = after get()")
|
||||
}
|
||||
miniSink?.onExitFunction(cc.currentPos())
|
||||
} else if (t.isId("set")) {
|
||||
val setStart = cc.currentPos()
|
||||
cc.next() // consume 'set'
|
||||
cc.requireToken(Token.Type.LPAREN)
|
||||
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
||||
cc.requireToken(Token.Type.RPAREN)
|
||||
miniSink?.onEnterFunction(null)
|
||||
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||
cc.skipWsTokens()
|
||||
val body = parseBlock()
|
||||
@ -3226,6 +3226,7 @@ class Compiler(
|
||||
} else {
|
||||
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
||||
}
|
||||
miniSink?.onExitFunction(cc.currentPos())
|
||||
} else if (t.isId("private") || t.isId("protected")) {
|
||||
val vis = if (t.isId("private")) Visibility.Private else Visibility.Protected
|
||||
val mark = cc.savePos()
|
||||
@ -3237,6 +3238,7 @@ class Compiler(
|
||||
cc.next() // consume '('
|
||||
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
||||
cc.requireToken(Token.Type.RPAREN)
|
||||
miniSink?.onEnterFunction(null)
|
||||
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||
cc.skipWsTokens()
|
||||
val body = parseBlock()
|
||||
@ -3261,6 +3263,7 @@ class Compiler(
|
||||
} else {
|
||||
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
||||
}
|
||||
miniSink?.onExitFunction(cc.currentPos())
|
||||
}
|
||||
} else {
|
||||
cc.restorePos(mark)
|
||||
|
||||
@ -34,18 +34,34 @@ class CompilerContext(val tokens: List<Token>) {
|
||||
}
|
||||
|
||||
var currentIndex = 0
|
||||
private var pendingGT = 0
|
||||
|
||||
fun hasNext() = currentIndex < tokens.size
|
||||
fun hasNext() = currentIndex < tokens.size || pendingGT > 0
|
||||
fun hasPrevious() = currentIndex > 0
|
||||
fun next() =
|
||||
if (currentIndex < tokens.size) tokens[currentIndex++]
|
||||
fun next(): Token {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
currentIndex = pos
|
||||
currentIndex = pos shr 2
|
||||
pendingGT = pos and 3
|
||||
}
|
||||
|
||||
fun ensureLabelIsValid(pos: Pos, label: String) {
|
||||
@ -106,12 +122,13 @@ class CompilerContext(val tokens: List<Token>) {
|
||||
errorMessage: String = "expected ${tokenType.name}",
|
||||
isOptional: Boolean = false
|
||||
): Boolean {
|
||||
val pos = savePos()
|
||||
val t = next()
|
||||
return if (t.type != tokenType) {
|
||||
if (!isOptional) {
|
||||
throw ScriptError(t.pos, errorMessage)
|
||||
} else {
|
||||
previous()
|
||||
restorePos(pos)
|
||||
false
|
||||
}
|
||||
} else true
|
||||
@ -122,20 +139,25 @@ class CompilerContext(val tokens: List<Token>) {
|
||||
* @return true if token was found and skipped
|
||||
*/
|
||||
fun skipNextIf(vararg types: Token.Type): Boolean {
|
||||
val pos = savePos()
|
||||
val t = next()
|
||||
return if (t.type in types)
|
||||
true
|
||||
else {
|
||||
previous()
|
||||
restorePos(pos)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
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 {
|
||||
@ -163,12 +185,13 @@ class CompilerContext(val tokens: List<Token>) {
|
||||
|
||||
|
||||
inline fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
|
||||
val pos = savePos()
|
||||
val t = next()
|
||||
return if (t.type == typeId) {
|
||||
f(t)
|
||||
true
|
||||
} else {
|
||||
previous()
|
||||
restorePos(pos)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,5 +27,6 @@ sealed class TypeDecl(val isNullable:Boolean = false) {
|
||||
object TypeAny : TypeDecl()
|
||||
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)
|
||||
}
|
||||
|
||||
@ -236,6 +236,7 @@ class ClassDocsBuilder internal constructor(private val className: String) {
|
||||
name = name,
|
||||
mutable = mutable,
|
||||
type = type?.toMiniTypeRef(),
|
||||
initRange = null,
|
||||
doc = md,
|
||||
nameStart = Pos.builtIn,
|
||||
isStatic = isStatic,
|
||||
@ -534,6 +535,9 @@ private fun buildStdlibDocs(): List<MiniDecl> {
|
||||
)
|
||||
|
||||
// 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(
|
||||
name = "launch",
|
||||
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")
|
||||
)
|
||||
|
||||
// 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)
|
||||
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
|
||||
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"))
|
||||
@ -591,7 +604,7 @@ private fun buildStdlibDocs(): List<MiniDecl> {
|
||||
}
|
||||
|
||||
// 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
|
||||
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."))
|
||||
@ -600,22 +613,22 @@ private fun buildStdlibDocs(): List<MiniDecl> {
|
||||
}
|
||||
|
||||
// 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.")
|
||||
}
|
||||
|
||||
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 = "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.
|
||||
method(name = "re", doc = StdlibInlineDocIndex.methodDoc("String", "re") ?: "Compile this string into a regular expression.", returns = type("lyng.Regex"))
|
||||
}
|
||||
|
||||
// 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.
|
||||
field(name = "sourceName", doc = "Source (file) name.", type = type("lyng.String"))
|
||||
field(name = "line", doc = "Line number (1-based).", type = type("lyng.Int"))
|
||||
|
||||
@ -23,6 +23,7 @@ package net.sergeych.lyng.miniast
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.binding.BindingSnapshot
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
import net.sergeych.lyng.pacman.ImportProvider
|
||||
|
||||
@ -58,7 +59,7 @@ object CompletionEngineLight {
|
||||
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
|
||||
StdlibDocsBootstrap.ensure()
|
||||
val prefix = prefixAt(text, caret)
|
||||
@ -73,6 +74,10 @@ object CompletionEngineLight {
|
||||
val memberDot = DocLookupUtils.findDotLeft(text, word?.first ?: caret)
|
||||
if (memberDot != null) {
|
||||
// 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 ->
|
||||
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||
return out
|
||||
@ -88,7 +93,7 @@ object CompletionEngineLight {
|
||||
return out
|
||||
}
|
||||
// 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)
|
||||
return out
|
||||
}
|
||||
@ -97,11 +102,16 @@ object CompletionEngineLight {
|
||||
}
|
||||
|
||||
// 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 classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
||||
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
|
||||
@ -274,33 +284,38 @@ object CompletionEngineLight {
|
||||
emitGroup(directMap)
|
||||
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 {
|
||||
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
||||
val ext = BuiltinDocRegistry.extensionMemberNamesFor(className)
|
||||
for (name in ext) {
|
||||
val extensions = DocLookupUtils.collectExtensionMemberNames(imported, className, mini)
|
||||
for (name in extensions) {
|
||||
if (already.contains(name)) continue
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name)
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name, mini)
|
||||
if (resolved != null) {
|
||||
when (val member = resolved.second) {
|
||||
val m = resolved.second
|
||||
val ci = when (m) {
|
||||
is MiniMemberFunDecl -> {
|
||||
val params = member.params.joinToString(", ") { it.name }
|
||||
val ci = CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(member.returnType))
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
already.add(name)
|
||||
val params = m.params.joinToString(", ") { it.name }
|
||||
CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType))
|
||||
}
|
||||
is MiniMemberValDecl -> {
|
||||
val ci = CompletionItem(name, Kind.Field, typeText = typeOf(member.type))
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
already.add(name)
|
||||
is MiniFunDecl -> {
|
||||
val params = m.params.joinToString(", ") { it.name }
|
||||
CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType))
|
||||
}
|
||||
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 {
|
||||
// Fallback: emit simple method name without detailed types
|
||||
val ci = CompletionItem(name, Kind.Method, tailText = "()", typeText = null)
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
already.add(name)
|
||||
if (ci.name.startsWith(prefix, true)) {
|
||||
out += ci
|
||||
already.add(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,14 +91,15 @@ object DocLookupUtils {
|
||||
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
|
||||
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) {
|
||||
if (d.name == name && src.offsetOf(d.nameStart) == startOffset) {
|
||||
if (d.name == name && matches(d.nameStart, d.name.length)) {
|
||||
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
|
||||
else -> null
|
||||
}
|
||||
@ -106,25 +107,27 @@ object DocLookupUtils {
|
||||
|
||||
if (d is MiniFunDecl) {
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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()
|
||||
}
|
||||
|
||||
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> {
|
||||
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)
|
||||
@ -232,24 +250,65 @@ object DocLookupUtils {
|
||||
for ((name, list) in buckets) {
|
||||
result[name] = mergeClassDecls(name, list)
|
||||
}
|
||||
// Root object alias
|
||||
if (result.containsKey("Obj") && !result.containsKey("Any")) {
|
||||
result["Any"] = result["Obj"]!!
|
||||
}
|
||||
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)
|
||||
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniMemberDecl>? {
|
||||
val cls = classes[name] ?: return null
|
||||
cls.members.firstOrNull { it.name == member }?.let { return name to it }
|
||||
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniNamedDecl>? {
|
||||
if (!visited.add(name)) return null
|
||||
for (baseName in cls.bases) {
|
||||
dfs(baseName, visited)?.let { return it }
|
||||
val cls = classes[name]
|
||||
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 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)
|
||||
// Preferred order for ambiguous common ops
|
||||
val preference = listOf("Iterable", "Iterator", "List")
|
||||
@ -301,6 +360,12 @@ object DocLookupUtils {
|
||||
if (mini == null) return null
|
||||
val i = prevNonWs(text, dotPos - 1)
|
||||
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 ident = text.substring(wordRange.first, wordRange.second)
|
||||
|
||||
@ -310,14 +375,14 @@ object DocLookupUtils {
|
||||
if (ref != null) {
|
||||
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
|
||||
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 }
|
||||
}
|
||||
} else {
|
||||
// Check if it's a declaration (e.g. static access to a class)
|
||||
val sym = binding.symbols.firstOrNull { it.declStart == wordRange.first && it.name == ident }
|
||||
if (sym != null) {
|
||||
val type = findTypeByRange(mini, sym.name, sym.declStart)
|
||||
val type = findTypeByRange(mini, sym.name, sym.declStart, text, imported)
|
||||
simpleClassNameOf(type)?.let { return it }
|
||||
// if it's a class/enum, return its name directly
|
||||
if (sym.kind == net.sergeych.lyng.binding.SymbolKind.Class || sym.kind == net.sergeych.lyng.binding.SymbolKind.Enum) return sym.name
|
||||
@ -325,13 +390,17 @@ object DocLookupUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// 1) Global declarations in current file (val/var/fun/class/enum)
|
||||
val d = mini.declarations.firstOrNull { it.name == ident }
|
||||
// 1) Declarations in current file (val/var/fun/class/enum), prioritized by proximity
|
||||
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) {
|
||||
return when (d) {
|
||||
is MiniClassDecl -> 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)
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
val dotBefore = findDotLeft(text, wordRange.first)
|
||||
if (dotBefore != null) {
|
||||
@ -353,7 +425,7 @@ object DocLookupUtils {
|
||||
if (resolved != null) {
|
||||
val rt = when (val m = resolved.second) {
|
||||
is MiniMemberFunDecl -> m.returnType
|
||||
is MiniMemberValDecl -> m.type
|
||||
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini)
|
||||
else -> null
|
||||
}
|
||||
return simpleClassNameOf(rt)
|
||||
@ -368,6 +440,43 @@ object DocLookupUtils {
|
||||
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? {
|
||||
if (mini == null) return null
|
||||
var i = prevNonWs(text, dotPos - 1)
|
||||
@ -420,7 +529,9 @@ object DocLookupUtils {
|
||||
val rt = when (m) {
|
||||
is MiniMemberFunDecl -> m.returnType
|
||||
is MiniMemberValDecl -> m.type
|
||||
is MiniInitDecl -> null
|
||||
is MiniFunDecl -> m.returnType
|
||||
is MiniValDecl -> m.type
|
||||
else -> null
|
||||
}
|
||||
simpleClassNameOf(rt)
|
||||
}
|
||||
@ -455,13 +566,25 @@ object DocLookupUtils {
|
||||
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 }
|
||||
var i = prevNonWs(text, dotPos - 1)
|
||||
if (i >= 0) {
|
||||
when (text[i]) {
|
||||
'"' -> 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"
|
||||
')' -> {
|
||||
// Parenthesized expression: walk back to matching '(' and inspect the inner expression
|
||||
@ -564,7 +687,9 @@ object DocLookupUtils {
|
||||
val ret = when (member) {
|
||||
is MiniMemberFunDecl -> member.returnType
|
||||
is MiniMemberValDecl -> member.type
|
||||
is MiniInitDecl -> null
|
||||
is MiniFunDecl -> member.returnType
|
||||
is MiniValDecl -> member.type
|
||||
else -> null
|
||||
}
|
||||
return simpleClassNameOf(ret)
|
||||
}
|
||||
@ -627,11 +752,216 @@ object DocLookupUtils {
|
||||
val ret = when (member) {
|
||||
is MiniMemberFunDecl -> member.returnType
|
||||
is MiniMemberValDecl -> member.type
|
||||
is MiniInitDecl -> null
|
||||
is MiniFunDecl -> member.returnType
|
||||
is MiniValDecl -> member.type
|
||||
else -> null
|
||||
}
|
||||
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) {
|
||||
null -> null
|
||||
is MiniTypeName -> t.segments.lastOrNull()?.name
|
||||
@ -667,13 +997,13 @@ object DocLookupUtils {
|
||||
fun enumToSyntheticClass(en: MiniEnumDecl): MiniClassDecl {
|
||||
val staticMembers = mutableListOf<MiniMemberDecl>()
|
||||
// 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
|
||||
staticMembers.add(MiniMemberFunDecl(en.range, "valueOf", listOf(MiniParam("name", null, en.nameStart)), null, null, en.nameStart, isStatic = true))
|
||||
|
||||
// Also add each entry as a static member (const)
|
||||
for (entry in 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(
|
||||
|
||||
@ -81,7 +81,7 @@ data class MiniTypeVar(
|
||||
) : MiniTypeRef
|
||||
|
||||
// Script and declarations (lean subset; can be extended later)
|
||||
sealed interface MiniDecl : MiniNode {
|
||||
sealed interface MiniNamedDecl : MiniNode {
|
||||
val name: String
|
||||
val doc: MiniDoc?
|
||||
// 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
|
||||
}
|
||||
|
||||
sealed interface MiniDecl : MiniNamedDecl
|
||||
|
||||
data class MiniScript(
|
||||
override val range: MiniRange,
|
||||
val declarations: MutableList<MiniDecl> = mutableListOf(),
|
||||
@ -172,12 +174,8 @@ data class MiniIdentifier(
|
||||
) : MiniNode
|
||||
|
||||
// --- Class member declarations (for built-in/registry docs) ---
|
||||
sealed interface MiniMemberDecl : MiniNode {
|
||||
val name: String
|
||||
val doc: MiniDoc?
|
||||
val nameStart: Pos
|
||||
sealed interface MiniMemberDecl : MiniNamedDecl {
|
||||
val isStatic: Boolean
|
||||
val isExtern: Boolean
|
||||
}
|
||||
|
||||
data class MiniMemberFunDecl(
|
||||
@ -189,6 +187,7 @@ data class MiniMemberFunDecl(
|
||||
override val nameStart: Pos,
|
||||
override val isStatic: Boolean = false,
|
||||
override val isExtern: Boolean = false,
|
||||
val body: MiniBlock? = null
|
||||
) : MiniMemberDecl
|
||||
|
||||
data class MiniMemberValDecl(
|
||||
@ -196,6 +195,7 @@ data class MiniMemberValDecl(
|
||||
override val name: String,
|
||||
val mutable: Boolean,
|
||||
val type: MiniTypeRef?,
|
||||
val initRange: MiniRange?,
|
||||
override val doc: MiniDoc?,
|
||||
override val nameStart: Pos,
|
||||
override val isStatic: Boolean = false,
|
||||
@ -222,6 +222,9 @@ interface MiniAstSink {
|
||||
fun onEnterClass(node: MiniClassDecl) {}
|
||||
fun onExitClass(end: Pos) {}
|
||||
|
||||
fun onEnterFunction(node: MiniFunDecl?) {}
|
||||
fun onExitFunction(end: Pos) {}
|
||||
|
||||
fun onImport(node: MiniImport) {}
|
||||
fun onFunDecl(node: MiniFunDecl) {}
|
||||
fun onValDecl(node: MiniValDecl) {}
|
||||
@ -254,6 +257,7 @@ class MiniAstBuilder : MiniAstSink {
|
||||
private val classStack = ArrayDeque<MiniClassDecl>()
|
||||
private var lastDoc: MiniDoc? = null
|
||||
private var scriptDepth: Int = 0
|
||||
private var functionDepth: Int = 0
|
||||
|
||||
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) {
|
||||
currentScript?.imports?.add(node)
|
||||
}
|
||||
@ -298,7 +310,7 @@ class MiniAstBuilder : MiniAstSink {
|
||||
override fun onFunDecl(node: MiniFunDecl) {
|
||||
val attach = node.copy(doc = node.doc ?: lastDoc)
|
||||
val currentClass = classStack.lastOrNull()
|
||||
if (currentClass != null) {
|
||||
if (currentClass != null && functionDepth == 0) {
|
||||
// Convert MiniFunDecl to MiniMemberFunDecl for inclusion in members
|
||||
val member = MiniMemberFunDecl(
|
||||
range = attach.range,
|
||||
@ -308,13 +320,29 @@ class MiniAstBuilder : MiniAstSink {
|
||||
doc = attach.doc,
|
||||
nameStart = attach.nameStart,
|
||||
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)
|
||||
classStack.removeLast()
|
||||
classStack.addLast(currentClass.copy(members = currentClass.members + member))
|
||||
// Check if we already have this member (from a previous onFunDecl call for the same function)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
@ -322,21 +350,36 @@ class MiniAstBuilder : MiniAstSink {
|
||||
override fun onValDecl(node: MiniValDecl) {
|
||||
val attach = node.copy(doc = node.doc ?: lastDoc)
|
||||
val currentClass = classStack.lastOrNull()
|
||||
if (currentClass != null) {
|
||||
if (currentClass != null && functionDepth == 0) {
|
||||
val member = MiniMemberValDecl(
|
||||
range = attach.range,
|
||||
name = attach.name,
|
||||
mutable = attach.mutable,
|
||||
type = attach.type,
|
||||
initRange = attach.initRange,
|
||||
doc = attach.doc,
|
||||
nameStart = attach.nameStart,
|
||||
isStatic = false, // TODO: track static if needed
|
||||
isExtern = attach.isExtern
|
||||
)
|
||||
classStack.removeLast()
|
||||
classStack.addLast(currentClass.copy(members = currentClass.members + member))
|
||||
// Duplicates for vals are rare but possible if Compiler calls it twice
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@ -4,8 +4,6 @@
|
||||
*/
|
||||
package net.sergeych.lyng.miniast
|
||||
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
|
||||
object StdlibDocsBootstrap {
|
||||
// Simple idempotent guard; races are harmless as initializer side-effects are idempotent
|
||||
private var ensured = false
|
||||
@ -16,7 +14,25 @@ object StdlibDocsBootstrap {
|
||||
// Touch core Obj* types whose docs are registered via addFnDoc/addConstDoc
|
||||
// Accessing .type forces their static initializers to run and register docs.
|
||||
@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) {
|
||||
// Best-effort; absence should not break consumers
|
||||
} finally {
|
||||
|
||||
@ -25,6 +25,9 @@ import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.serializer
|
||||
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.LynonEncoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
@ -524,20 +527,46 @@ open class Obj {
|
||||
companion object {
|
||||
|
||||
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)
|
||||
}
|
||||
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()
|
||||
}
|
||||
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()))
|
||||
}
|
||||
// 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)))
|
||||
}
|
||||
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()
|
||||
(thisObj as? ObjInstance)?.let {
|
||||
body.callOn(ApplyScope(this, it.instanceScope))
|
||||
@ -546,11 +575,21 @@ open class Obj {
|
||||
}
|
||||
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)))
|
||||
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)
|
||||
}
|
||||
addFn("getAt") {
|
||||
@ -563,7 +602,12 @@ open class Obj {
|
||||
thisObj.putAt(this, requiredArg<Obj>(0), 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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ package net.sergeych.lyng.obj
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.miniast.addFnDoc
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
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")
|
||||
}
|
||||
}.apply {
|
||||
addFn("toInt") {
|
||||
addFnDoc(
|
||||
name = "toInt",
|
||||
doc = "Returns this integer (identity operation).",
|
||||
returns = net.sergeych.lyng.miniast.type("lyng.Int"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
thisObj
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -274,6 +275,89 @@ class MiniAstTest {
|
||||
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
|
||||
fun miniAst_captures_user_sample_extern_doc() = runTest {
|
||||
val code = """
|
||||
@ -311,4 +395,48 @@ class MiniAstTest {
|
||||
assertEquals("O3", resolved.first)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,4 +174,308 @@ class CompletionEngineLightTest {
|
||||
val ns = names(items)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user