Complete symbols loaded by imported modules

This commit is contained in:
hgranthorner
2026-08-05 16:44:16 -04:00
parent 98e25c5468
commit e7f9d60be8
2 changed files with 52 additions and 8 deletions

View File

@@ -50,6 +50,7 @@ internal object JaiSymbolResolver {
private data class ImportBinding(
val file: PsiFile,
val directive: String,
val scope: PsiElement?,
val visibleFrom: Int,
val boundName: String?,
@@ -244,6 +245,7 @@ internal object JaiSymbolResolver {
ImportBinding(
file = target,
directive = name,
scope = PsiTreeUtil.getParentOfType(literal, JaiBlock::class.java),
visibleFrom = literal.textOffset,
boundName = boundName,
@@ -288,6 +290,7 @@ internal object JaiSymbolResolver {
val targetPsi = PsiManager.getInstance(file.project).findFile(target) ?: return@mapNotNull null
ImportBinding(
file = targetPsi,
directive = directive,
scope = null,
visibleFrom = match.range.first,
boundName = null,
@@ -331,7 +334,22 @@ internal object JaiSymbolResolver {
.toSet()
}
private fun topLevelDeclarations(file: PsiFile): List<JaiDeclName> =
/**
* Returns declarations exported by a file, including files it textually loads. A module's
* public surface is often assembled by loading several sibling files (for example, Basic
* loads Print.jai). Do not follow #import here: an imported module's private dependencies
* must not become unqualified candidates in the importing file.
*/
private fun topLevelDeclarations(file: PsiFile): List<JaiDeclName> = topLevelDeclarations(file, linkedSetOf())
private fun topLevelDeclarations(
file: PsiFile,
visited: MutableSet<String>,
): List<JaiDeclName> {
val fileKey = file.virtualFile?.path ?: file.name
if (!visited.add(fileKey)) return emptyList()
val directDeclarations =
PsiTreeUtil
.findChildrenOfType(file, JaiDeclaration::class.java)
.asSequence()
@@ -340,6 +358,16 @@ internal object JaiSymbolResolver {
.sortedBy { it.textOffset }
.toList()
val loadedDeclarations =
importBindings(file)
.asSequence()
.filter { it.directive == "#load" }
.flatMap { topLevelDeclarations(it.file, visited).asSequence() }
.toList()
return directDeclarations + loadedDeclarations
}
private fun isVisible(
symbol: ScopedSymbol,
position: PsiElement,

View File

@@ -111,6 +111,22 @@ class JaiCompletionTest : BasePlatformTestCase() {
assertTrue("expected alloc in $strings", strings!!.contains("alloc"))
}
fun testCompletesSymbolsLoadedByImportedModule() {
myFixture.configureByText(
"nested-module-consumer.jai",
"""
#import "Basic";
main :: () { pri<caret>("hello"); }
""".trimIndent(),
)
myFixture.complete(CompletionType.BASIC, 1)
assertTrue(
"expected print from a file loaded by the imported Basic module: ${myFixture.lookupElementStrings}",
myFixture.lookupElementStrings.orEmpty().contains("print"),
)
}
fun testCompletesSymbolsFromLoadedFile() {
myFixture.addFileToProject("library.jai", "Helper :: () {}")
myFixture.configureByText(