From a6492bb750a1d0a5c0d700563b79f2d357e5bd48 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 15 Mar 2026 09:13:33 +0300 Subject: [PATCH] Add support for symbol inclusion via directive and refactor extern classes handling --- .../sergeych/lyng/idea/util/LyngAstManager.kt | 21 +++++++- .../definitions/LyngDefinitionFilesTest.kt | 43 +++++++++++++++ lynglib/build.gradle.kts | 2 +- lynglib/stdlib/lyng/root.lyng | 54 +++++++++---------- 4 files changed, 90 insertions(+), 30 deletions(-) diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt index ae69df8..f16a0ad 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt @@ -36,6 +36,7 @@ object LyngAstManager { private val STAMP_KEY = Key.create("lyng.mini.cache.stamp") private val ANALYSIS_KEY = Key.create("lyng.analysis.cache") private val implicitBuiltinNames = setOf("void") + private val includeSymbolsDirective = Regex("""(?im)^\s*//\s*include\s+symbols\s*:\s*(.+?)\s*$""") fun getMiniAst(file: PsiFile): MiniScript? = runReadAction { getAnalysis(file)?.mini @@ -44,8 +45,8 @@ object LyngAstManager { fun getCombinedStamp(file: PsiFile): Long = runReadAction { var combinedStamp = file.viewProvider.modificationStamp if (!file.name.endsWith(".lyng.d")) { - collectDeclarationFiles(file).forEach { df -> - combinedStamp += df.viewProvider.modificationStamp + collectDeclarationFiles(file).forEach { symbolsFile -> + combinedStamp += symbolsFile.viewProvider.modificationStamp } } combinedStamp @@ -66,6 +67,22 @@ object LyngAstManager { currentDir = currentDir.parentDirectory } + val includeSpecs = includeSymbolsDirective.findAll(file.viewProvider.contents) + .flatMap { it.groupValues[1].split(',').asSequence() } + .map { it.trim() } + .filter { it.isNotEmpty() } + .toList() + val baseDir = file.virtualFile?.parent + if (baseDir != null) { + for (spec in includeSpecs) { + val included = baseDir.findFileByRelativePath(spec) ?: continue + if (included.path == file.virtualFile?.path) continue + if (seen.add(included.path)) { + psiManager.findFile(included)?.let { result.add(it) } + } + } + } + if (result.isNotEmpty()) return@runReadAction result // Fallback for virtual/light files without a stable parent chain (e.g., tests) diff --git a/lyng-idea/src/test/kotlin/net/sergeych/lyng/idea/definitions/LyngDefinitionFilesTest.kt b/lyng-idea/src/test/kotlin/net/sergeych/lyng/idea/definitions/LyngDefinitionFilesTest.kt index 5913396..91a7f61 100644 --- a/lyng-idea/src/test/kotlin/net/sergeych/lyng/idea/definitions/LyngDefinitionFilesTest.kt +++ b/lyng-idea/src/test/kotlin/net/sergeych/lyng/idea/definitions/LyngDefinitionFilesTest.kt @@ -50,6 +50,18 @@ class LyngDefinitionFilesTest : BasePlatformTestCase() { myFixture.addFileToProject("api.lyng.d", defs) } + private fun addPlainSymbolsFile() { + val defs = """ + /** Symbols exposed via include directive */ + class PlainDeclared(val name: String) { + fun hello(): String = "ok" + } + + fun plainTopFun(x: Int): Int = x + 2 + """.trimIndent() + myFixture.addFileToProject("plain_symbols.lyng", defs) + } + fun test_CompletionsIncludeDefinitions() { addDefinitionsFile() enableCompletion() @@ -136,4 +148,35 @@ class LyngDefinitionFilesTest : BasePlatformTestCase() { val messages = analysis?.diagnostics?.map { it.message } ?: emptyList() assertTrue("Should not report unresolved name for void, got=$messages", messages.none { it.contains("unresolved name: void") }) } + + fun test_CompletionsIncludePlainLyngViaDirective() { + addPlainSymbolsFile() + enableCompletion() + val code = """ + // include symbols: plain_symbols.lyng + val v = plainTop + """.trimIndent() + myFixture.configureByText("main.lyng", code) + val text = myFixture.editor.document.text + val caret = myFixture.caretOffset + val analysis = LyngAstManager.getAnalysis(myFixture.file) + val engine = runBlocking { CompletionEngineLight.completeSuspend(text, caret, analysis?.mini, analysis?.binding).map { it.name } } + assertTrue("Expected plainTopFun from included .lyng; got=$engine", engine.contains("plainTopFun")) + } + + fun test_DiagnosticsIgnorePlainLyngSymbolsViaDirective() { + addPlainSymbolsFile() + val code = """ + // include symbols: plain_symbols.lyng + val x = plainTopFun(1) + val y = PlainDeclared("x") + y.hello() + """.trimIndent() + myFixture.configureByText("main.lyng", code) + val analysis = LyngAstManager.getAnalysis(myFixture.file) + val messages = analysis?.diagnostics?.map { it.message } ?: emptyList() + assertTrue("Should not report unresolved name for plainTopFun", messages.none { it.contains("unresolved name: plainTopFun") }) + assertTrue("Should not report unresolved name for PlainDeclared", messages.none { it.contains("unresolved name: PlainDeclared") }) + assertTrue("Should not report unresolved member for hello", messages.none { it.contains("unresolved member: hello") }) + } } diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index daf1234..d25f22b 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.5.0-SNAPSHOT" +version = "1.5.2-SNAPSHOT" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index 16b6c51..9c5ce87 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -8,22 +8,22 @@ extern class IllegalArgumentException extern class NotImplementedException extern class Delegate extern class Iterable { - extern fun iterator(): Iterator - extern fun forEach(action: (T)->void): void - extern fun map(transform: (T)->R): List - extern fun toList(): List - extern fun toImmutableList(): ImmutableList - extern val toSet: Set - extern val toImmutableSet: ImmutableSet - extern val toMap: Map - extern val toImmutableMap: ImmutableMap + fun iterator(): Iterator + fun forEach(action: (T)->void): void + fun map(transform: (T)->R): List + fun toList(): List + fun toImmutableList(): ImmutableList + val toSet: Set + val toImmutableSet: ImmutableSet + val toMap: Map + val toImmutableMap: ImmutableMap } extern class Iterator { - extern fun hasNext(): Bool - extern fun next(): T - extern fun cancelIteration(): void - extern fun toList(): List + fun hasNext(): Bool + fun next(): T + fun cancelIteration(): void + fun toList(): List } // Host-provided iterator wrapper for Kotlin collections. @@ -33,47 +33,47 @@ class KotlinIterator : Iterator { } extern class Collection : Iterable { - extern val size: Int + val size: Int } extern class Array : Collection { } extern class ImmutableList : Array { - extern fun toMutable(): List + fun toMutable(): List } extern class List : Array { - extern fun add(value: T, more...): void - extern fun toImmutable(): ImmutableList + fun add(value: T, more...): void + fun toImmutable(): ImmutableList } extern class RingBuffer : Iterable { - extern val size: Int - extern fun first(): T - extern fun add(value: T): void + val size: Int + fun first(): T + fun add(value: T): void } extern class Set : Collection { - extern fun toImmutable(): ImmutableSet + fun toImmutable(): ImmutableSet } extern class ImmutableSet : Collection { - extern fun toMutable(): Set + fun toMutable(): Set } extern class Map : Collection> { - extern fun toImmutable(): ImmutableMap + fun toImmutable(): ImmutableMap } extern class ImmutableMap : Collection> { - extern fun getOrNull(key: K): V? - extern fun toMutable(): Map + fun getOrNull(key: K): V? + fun toMutable(): Map } extern class MapEntry : Array { - extern val key: K - extern val value: V + val key: K + val value: V } // Built-in math helpers (implemented in host runtime).