fixed autocompletion for class constructor parameters

This commit is contained in:
Sergey Chernov 2026-01-06 12:08:23 +01:00
parent 72bb6ae67b
commit 3ef68d8bb4
4 changed files with 112 additions and 39 deletions

View File

@ -30,7 +30,6 @@ import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiFile
import com.intellij.util.ProcessingContext
import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.idea.LyngLanguage
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
@ -144,11 +143,6 @@ class LyngCompletionContributor : CompletionContributor() {
}
}
// In global context, add params in scope first (engine does not include them)
if (memberDotPos == null && mini != null) {
offerParamsInScope(emit, mini, text, caret)
}
// Render engine items
for (ci in engineItems) {
val builder = when (ci.kind) {
@ -167,7 +161,7 @@ class LyngCompletionContributor : CompletionContributor() {
Kind.Enum -> LookupElementBuilder.create(ci.name)
.withIcon(AllIcons.Nodes.Enum)
Kind.Value -> LookupElementBuilder.create(ci.name)
.withIcon(AllIcons.Nodes.Field)
.withIcon(AllIcons.Nodes.Variable)
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
Kind.Field -> LookupElementBuilder.create(ci.name)
.withIcon(AllIcons.Nodes.Field)
@ -545,27 +539,6 @@ class LyngCompletionContributor : CompletionContributor() {
}
}
// --- MiniAst-based inference helpers ---
private fun offerParamsInScope(emit: (com.intellij.codeInsight.lookup.LookupElement) -> Unit, mini: MiniScript, text: String, caret: Int) {
val src = mini.range.start.source
// Find function whose body contains caret or whose whole range contains caret
val fns = mini.declarations.filterIsInstance<MiniFunDecl>()
for (fn in fns) {
val start = src.offsetOf(fn.range.start)
val end = src.offsetOf(fn.range.end).coerceAtMost(text.length)
if (caret in start..end) {
for (p in fn.params) {
val builder = LookupElementBuilder.create(p.name)
.withIcon(AllIcons.Nodes.Variable)
.withTypeText(typeOf(p.type), true)
emit(builder)
}
return
}
}
}
// Lenient textual import extractor (duplicated from QuickDoc privately)
private fun extractImportsFromText(text: String): List<String> {
val result = LinkedHashSet<String>()

View File

@ -2030,20 +2030,18 @@ class Compiler(
run {
val declRange = MiniRange(startPos, cc.currentPos())
val bases = baseSpecs.map { it.name }
// Collect constructor fields declared as val/var in primary constructor
// Collect constructor fields declared in primary constructor
val ctorFields = mutableListOf<MiniCtorField>()
constructorArgsDeclaration?.let { ad ->
for (p in ad.params) {
val at = p.accessType
if (at != null) {
val mutable = at == AccessType.Var
ctorFields += MiniCtorField(
name = p.name,
mutable = mutable,
type = p.miniType,
nameStart = p.pos
)
}
val mutable = at == AccessType.Var
ctorFields += MiniCtorField(
name = p.name,
mutable = mutable,
type = p.miniType,
nameStart = p.pos
)
}
}
val node = MiniClassDecl(

View File

@ -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.highlight.offsetOf
import net.sergeych.lyng.pacman.ImportProvider
/** Minimal completion item description (IDE-agnostic). */
@ -96,7 +97,11 @@ object CompletionEngineLight {
}
// Global identifiers: params > local decls > imported > stdlib; Functions > Classes > Values; alphabetical
val decls = mini.declarations
if (mini != null) {
offerParamsInScope(out, prefix, mini, text, caret)
}
val decls = mini?.declarations ?: emptyList()
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() }
@ -130,6 +135,74 @@ object CompletionEngineLight {
// --- Emission helpers ---
private fun offerParamsInScope(out: MutableList<CompletionItem>, prefix: String, mini: MiniScript, text: String, caret: Int) {
val src = mini.range.start.source
val already = mutableSetOf<String>()
fun add(ci: CompletionItem) {
if (ci.name.startsWith(prefix, true) && already.add(ci.name)) {
out.add(ci)
}
}
fun checkNode(node: Any) {
val range: MiniRange = when (node) {
is MiniDecl -> node.range
is MiniMemberDecl -> node.range
else -> return
}
val start = src.offsetOf(range.start)
val end = src.offsetOf(range.end).coerceAtMost(text.length)
if (caret in start..end) {
when (node) {
is MiniFunDecl -> {
for (p in node.params) {
add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type)))
}
}
is MiniClassDecl -> {
// Propose constructor parameters (ctorFields)
for (p in node.ctorFields) {
add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type)))
}
// Propose class-level fields
for (p in node.classFields) {
add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type)))
}
// Process members (methods/fields)
for (m in node.members) {
// If the member itself contains the caret (like a method), recurse
checkNode(m)
// Also offer the member itself for the class scope
when (m) {
is MiniMemberFunDecl -> {
val params = m.params.joinToString(", ") { it.name }
add(CompletionItem(m.name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType)))
}
is MiniMemberValDecl -> {
add(CompletionItem(m.name, if (m.mutable) Kind.Value else Kind.Field, typeText = typeOf(m.type)))
}
is MiniInitDecl -> {}
}
}
}
is MiniMemberFunDecl -> {
for (p in node.params) {
add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type)))
}
}
else -> {}
}
}
}
for (decl in mini.declarations) {
checkNode(decl)
}
}
private fun offerDeclAdd(out: MutableList<CompletionItem>, prefix: String, d: MiniDecl) {
fun add(ci: CompletionItem) { if (ci.name.startsWith(prefix, true)) out += ci }
when (d) {

View File

@ -145,4 +145,33 @@ class CompletionEngineLightTest {
// Should contain some iterator members
assertTrue(ns.isNotEmpty(), "Iterator members should be suggested after lines() with shebang present")
}
@Test
fun constructorParametersInMethod() = runBlocking {
val code = """
class MyClass(myParam) {
fun myMethod() {
myp<caret>
}
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("myParam"), "Constructor parameter 'myParam' should be proposed, but got: $ns")
}
@Test
fun classFieldsInMethod() = runBlocking {
val code = """
class MyClass {
val myField = 1
fun myMethod() {
myf<caret>
}
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("myField"), "Class field 'myField' should be proposed, but got: $ns")
}
}