Compare commits

...

2 Commits

Author SHA1 Message Date
3ef68d8bb4 fixed autocompletion for class constructor parameters 2026-01-06 12:08:23 +01:00
72bb6ae67b formatter fix on properties 2026-01-06 12:00:04 +01:00
6 changed files with 171 additions and 43 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,12 +2030,11 @@ 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,
@ -2045,7 +2044,6 @@ class Compiler(
)
}
}
}
val node = MiniClassDecl(
range = declRange,
name = nameToken.value,

View File

@ -42,6 +42,31 @@ object LyngFormatter {
return false
}
private fun isAccessorRelated(code: String): Boolean {
val t = code.trim()
if (t.isEmpty()) return false
if (isPropertyAccessor(t)) return true
// If it contains 'fun' or 'fn' as a word, it's probably a function declaration, not an accessor
if (Regex("\\b(fun|fn)\\b").containsMatchIn(t)) return false
val hasDecl = startsWithWord(t, "var") || startsWithWord(t, "val") ||
startsWithWord(t, "private") || startsWithWord(t, "protected") ||
startsWithWord(t, "override") || startsWithWord(t, "public")
if (hasDecl) {
val getSetMatch = Regex("\\b(get|set)\\b").find(t)
if (getSetMatch != null) {
// Check it's not part of an assignment to the property itself (e.g. val x = get())
val equalIndex = t.indexOf('=')
if (equalIndex == -1 || equalIndex > getSetMatch.range.first) {
return true
}
}
}
return false
}
/** Returns the input with indentation recomputed from scratch, line by line. */
fun reindent(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
// Normalize tabs to spaces globally before any transformation; results must contain no tabs
@ -82,7 +107,7 @@ object LyngFormatter {
if (isIf || isElseIf || isElse) return true
// property accessors ending with ) or =
if (isPropertyAccessor(t)) {
if (isAccessorRelated(t)) {
return if (t.contains('=')) t.endsWith('=') else t.endsWith(')')
}
return false
@ -168,7 +193,8 @@ object LyngFormatter {
}
val newBlockLevel = blockLevel
if (newBlockLevel > oldBlockLevel) {
val addedThisLine = (if (applyAwaiting) awaitingExtraIndent else 0) + (if (isAccessor) 1 else 0)
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code))
val addedThisLine = (if (applyAwaiting) awaitingExtraIndent else 0) + (if (isAccessorRelatedLine) 1 else 0)
repeat(newBlockLevel - oldBlockLevel) {
extraIndents.add(addedThisLine)
}
@ -186,7 +212,8 @@ object LyngFormatter {
val endsWithBrace = code.trimEnd().endsWith("{")
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
// It's another header, increment
awaitingExtraIndent += if (isAccessor) 2 else 1
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code))
awaitingExtraIndent += if (isAccessorRelatedLine) 2 else 1
} else {
// It's the body, reset
awaitingExtraIndent = 0
@ -195,7 +222,8 @@ object LyngFormatter {
// start awaiting if current line is a control header without '{'
val endsWithBrace = code.trimEnd().endsWith("{")
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
awaitingExtraIndent = if (isAccessor) 2 else 1
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code))
awaitingExtraIndent = if (isAccessorRelatedLine) 2 else 1
}
}

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

@ -859,4 +859,31 @@ class LyngFormatterTest {
""".trimIndent()
assertEquals(expected2, LyngFormatter.reindent(src2, LyngFormatConfig(indentSize = 4)))
}
@Test
fun mixedPropertyAccessors() {
val src = """
class X {
var x get() {
11
}
set(value) {
}
}
""".trimIndent()
val expected = """
class X {
var x get() {
11
}
set(value) {
}
}
""".trimIndent()
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
val out = LyngFormatter.reindent(src, cfg)
assertEquals(expected, out)
}
}

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