From 3ef68d8bb47499827144961dfeec9b58abc03959 Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 6 Jan 2026 12:08:23 +0100 Subject: [PATCH] fixed autocompletion for class constructor parameters --- .../completion/LyngCompletionContributor.kt | 29 +------ .../kotlin/net/sergeych/lyng/Compiler.kt | 18 ++--- .../lyng/miniast/CompletionEngineLight.kt | 75 ++++++++++++++++++- .../lyng/miniast/CompletionEngineLightTest.kt | 29 +++++++ 4 files changed, 112 insertions(+), 39 deletions(-) diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt index 16a3b6b..095cfa2 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt @@ -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() - 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 { val result = LinkedHashSet() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 68f6cb9..e90f611 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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() 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( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt index 774a44c..47f8b22 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt @@ -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().sortedBy { it.name.lowercase() } val classes = decls.filterIsInstance().sortedBy { it.name.lowercase() } val enums = decls.filterIsInstance().sortedBy { it.name.lowercase() } @@ -130,6 +135,74 @@ object CompletionEngineLight { // --- Emission helpers --- + private fun offerParamsInScope(out: MutableList, prefix: String, mini: MiniScript, text: String, caret: Int) { + val src = mini.range.start.source + val already = mutableSetOf() + + 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, prefix: String, d: MiniDecl) { fun add(ci: CompletionItem) { if (ci.name.startsWith(prefix, true)) out += ci } when (d) { diff --git a/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt b/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt index 80ed9ca..e66aafa 100644 --- a/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt +++ b/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt @@ -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 + } + } + """.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 + } + } + """.trimIndent() + val items = CompletionEngineLight.completeAtMarkerSuspend(code) + val ns = names(items) + assertTrue(ns.contains("myField"), "Class field 'myField' should be proposed, but got: $ns") + } }