diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt index 62f9d9d..9c950a5 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt @@ -27,9 +27,9 @@ import kotlinx.coroutines.runBlocking import net.sergeych.lyng.highlight.offsetOf import net.sergeych.lyng.idea.LyngFileType import net.sergeych.lyng.idea.util.LyngAstManager +import net.sergeych.lyng.idea.util.LyngIdeaImportProvider import net.sergeych.lyng.idea.util.TextCtx import net.sergeych.lyng.miniast.* -import net.sergeych.lyng.tools.IdeLenientImportProvider import net.sergeych.lyng.tools.LyngAnalysisRequest import net.sergeych.lyng.tools.LyngLanguageTools @@ -273,7 +273,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase("lyng.mini.cache") @@ -142,7 +145,8 @@ object LyngAstManager { val text = file.viewProvider.contents.toString() val built = try { - val provider = IdeLenientImportProvider.create() + DocsBootstrap.ensure() + val provider = LyngIdeaImportProvider.create() runBlocking { LyngLanguageTools.analyze( LyngAnalysisRequest(text = text, fileName = file.name, importProvider = provider) @@ -165,7 +169,7 @@ object LyngAstManager { val dMini = getAnalysis(df)?.mini ?: run { val dText = df.viewProvider.contents.toString() try { - val provider = IdeLenientImportProvider.create() + val provider = LyngIdeaImportProvider.create() runBlocking { LyngLanguageTools.analyze( LyngAnalysisRequest(text = dText, fileName = df.name, importProvider = provider) diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngIdeaImportProvider.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngIdeaImportProvider.kt new file mode 100644 index 0000000..0e7005a --- /dev/null +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngIdeaImportProvider.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.sergeych.lyng.idea.util + +import net.sergeych.lyng.ModuleScope +import net.sergeych.lyng.Pos +import net.sergeych.lyng.Scope +import net.sergeych.lyng.Script +import net.sergeych.lyng.io.console.createConsoleModule +import net.sergeych.lyng.io.fs.createFs +import net.sergeych.lyng.io.http.createHttpModule +import net.sergeych.lyng.io.net.createNetModule +import net.sergeych.lyng.io.process.createProcessModule +import net.sergeych.lyng.io.ws.createWsModule +import net.sergeych.lyng.pacman.ImportManager +import net.sergeych.lyng.pacman.ImportProvider +import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy +import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy +import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy +import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy +import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy +import net.sergeych.lyngio.ws.security.PermitAllWsAccessPolicy + +/** + * IDE import provider that knows about optional LyngIO modules used by editor analysis. + * + * The default import manager only exposes core modules; editor features need the pluggable + * `lyng.io.*` packages available as well so imported symbols resolve without false errors. + */ +class LyngIdeaImportProvider private constructor(root: Scope) : ImportProvider(root) { + override suspend fun createModuleScope(pos: Pos, packageName: String): ModuleScope { + return try { + baseImportManager.createModuleScope(pos, packageName) + } catch (_: Throwable) { + ModuleScope(this, pos, packageName) + } + } + + companion object { + private val baseImportManager: ImportManager by lazy { + Script.defaultImportManager.copy().apply { + createFs(PermitAllAccessPolicy, this) + createConsoleModule(PermitAllConsoleAccessPolicy, this) + createHttpModule(PermitAllHttpAccessPolicy, this) + createWsModule(PermitAllWsAccessPolicy, this) + createNetModule(PermitAllNetAccessPolicy, this) + createProcessModule(PermitAllProcessAccessPolicy, this) + } + } + + fun create(): LyngIdeaImportProvider = LyngIdeaImportProvider(baseImportManager.rootScope) + } +} 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 91a7f61..d466cf2 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 @@ -179,4 +179,20 @@ class LyngDefinitionFilesTest : BasePlatformTestCase() { 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") }) } + + fun test_DiagnosticsResolveOptionalNetModuleSymbols() { + val code = """ + import lyng.io.net + + val server = Net.tcpListen(0, "127.0.0.1") + val port = server.localAddress().port + """.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 Net, got=$messages", messages.none { it.contains("unresolved name: Net") }) + assertTrue("Should not report unresolved member for tcpListen, got=$messages", messages.none { it.contains("unresolved member: tcpListen") }) + assertTrue("Should not report unresolved member for localAddress, got=$messages", messages.none { it.contains("unresolved member: localAddress") }) + assertTrue("Should not report unresolved member for port, got=$messages", messages.none { it.contains("unresolved member: port") }) + } }