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

This commit is contained in:
sergeych committed 2026-01-07 02:55:47 +01:00
1 parent aba0048a83
commit fe5dded7af
17 files changed
+1195 -182

No files matched your search

@@ -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)))
}