Phase 5: add completion and symbol navigation

This commit is contained in:
hgranthorner
2026-08-04 15:25:11 -04:00
parent c4e1d92c9a
commit 4758bf12e2
8 changed files with 696 additions and 11 deletions

View File

@@ -0,0 +1,61 @@
package dev.hgh.jai.completion
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.testFramework.fixtures.BasePlatformTestCase
class JaiCompletionTest : BasePlatformTestCase() {
fun testCompletesKeywords() {
myFixture.configureByText("keyword.jai", "ret<caret>")
myFixture.completeBasic()
myFixture.checkResult("return")
}
fun testCompletesDirectivesWithHash() {
myFixture.configureByText("directive.jai", "#imp<caret>")
myFixture.completeBasic()
myFixture.checkResult("#import")
}
fun testDoesNotCompleteInsideStringLiterals() {
myFixture.configureByText("string.jai", "#char \"ret<caret>\";")
myFixture.complete(CompletionType.BASIC, 1)
assertTrue("strings should not offer keyword completion", myFixture.lookupElementStrings.isNullOrEmpty())
}
fun testDoesNotOfferModulesForStringImport() {
myFixture.configureByText("string-import.jai", "#import,string \"Bas<caret>\";")
myFixture.complete(CompletionType.BASIC, 1)
assertTrue("#import,string should not offer file modules", myFixture.lookupElementStrings.isNullOrEmpty())
}
fun testCompletesImportModulesFromLocalJaiModulesDirectory() {
myFixture.configureByText("main.jai", "#import \"Bas<caret>\";")
val strings = completeStrings()
assertNotNull("module completion should be available", strings)
assertTrue("expected Basic in $strings", strings!!.contains("Basic"))
assertTrue("expected Base64 in $strings", strings.contains("Base64"))
assertFalse("prefix should filter Bucket_Array from $strings", strings.contains("Bucket_Array"))
assertFalse("prefix should filter Bit_Array from $strings", strings.contains("Bit_Array"))
}
fun testCompletesLoadFilesRelativeToSource() {
myFixture.addFileToProject("other.jai", "value := 1;")
myFixture.configureByText("main.jai", "#load \"oth<caret>\";")
myFixture.completeBasic()
myFixture.checkResult("#load \"other.jai\";")
}
private fun completeStrings(): List<String>? {
myFixture.complete(CompletionType.BASIC, 1)
return myFixture.lookupElementStrings
}
}

View File

@@ -2,6 +2,7 @@ package dev.hgh.jai.reference
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiReference
@@ -9,6 +10,7 @@ import com.intellij.psi.PsiReferenceService
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import dev.hgh.jai.psi.JaiLiteralExpr
import dev.hgh.jai.psi.JaiRefExpr
class JaiReferenceTest : BasePlatformTestCase() {
fun testLoadResolvesAFileRelativeToTheSource() {
@@ -24,6 +26,117 @@ class JaiReferenceTest : BasePlatformTestCase() {
assertEquals(TextRange(1, literal.textLength - 1), reference.rangeInElement)
}
fun testProcedureReferenceResolvesToSameFileDeclaration() {
val source =
myFixture.addFileToProject(
"procedures.jai",
"""
helper :: () {}
main :: () { helper(); }
""".trimIndent(),
)
val referenceElement =
PsiTreeUtil
.findChildrenOfType(source, JaiRefExpr::class.java)
.single { it.text == "helper" }
val reference = onlyReference(referenceElement)
assertEquals("helper", reference.resolve()?.text)
assertEquals(TextRange(0, referenceElement.textLength), reference.rangeInElement)
}
fun testTypeReferenceResolvesToSameFileDeclaration() {
val source =
myFixture.addFileToProject(
"types.jai",
"""
Point :: struct { x: int; }
use_point :: (point: Point) -> int { return point.x; }
""".trimIndent(),
)
val referenceElement =
PsiTreeUtil
.findChildrenOfType(source, JaiRefExpr::class.java)
.single { it.text == "Point" }
val reference = onlyReference(referenceElement)
assertEquals("Point", reference.resolve()?.text)
}
fun testSymbolsResolveThroughLoadedFile() {
val target =
myFixture.addFileToProject(
"library.jai",
"""
Helper :: () {}
Point :: struct { x: int; }
""".trimIndent(),
)
val source =
myFixture.addFileToProject(
"consumer.jai",
"""
#load "library.jai";
main :: (point: Point) { Helper(); }
""".trimIndent(),
)
val references = PsiTreeUtil.findChildrenOfType(source, JaiRefExpr::class.java)
val helperReference = references.single { it.text == "Helper" }
val pointReference = references.single { it.text == "Point" }
assertEquals(
target.virtualFile.path,
onlyReference(helperReference)
.resolve()
?.containingFile
?.virtualFile
?.path,
)
assertEquals(
target.virtualFile.path,
onlyReference(pointReference)
.resolve()
?.containingFile
?.virtualFile
?.path,
)
}
fun testImportedProcedureReferenceResolvesFromJaiModule() {
val source =
myFixture.addFileToProject(
"module-consumer.jai",
"""
#import "Basic";
main :: () { alloc(1); }
""".trimIndent(),
)
val referenceElement =
PsiTreeUtil
.findChildrenOfType(source, JaiRefExpr::class.java)
.single { it.text == "alloc" }
val resolved = onlyReference(referenceElement).resolve()
assertEquals("alloc", resolved?.text)
assertEquals(
"${System.getProperty("user.home")}/.local/jai/modules/Basic/module.jai",
resolved?.containingFile?.virtualFile?.path,
)
}
fun testUnresolvedSymbolReferenceHasNoTarget() {
val source = myFixture.addFileToProject("unresolved.jai", "main :: () { missing(); }")
val referenceElement =
PsiTreeUtil
.findChildrenOfType(source, JaiRefExpr::class.java)
.single { it.text == "missing" }
assertNull(onlyReference(referenceElement).resolve())
}
fun testImportResolvesAJaiModuleDirectory() {
val sourceVirtualFile =
LocalFileSystem
@@ -90,12 +203,12 @@ class JaiReferenceTest : BasePlatformTestCase() {
return literal!!
}
private fun onlyReference(literal: JaiLiteralExpr): PsiReference {
val references = references(literal)
assertEquals("expected exactly one path reference", 1, references.size)
private fun onlyReference(element: PsiElement): PsiReference {
val references = references(element)
assertEquals("expected exactly one reference", 1, references.size)
return references.single()
}
private fun references(literal: JaiLiteralExpr): List<PsiReference> =
PsiReferenceService.getService().getContributedReferences(literal).toList()
private fun references(element: PsiElement): List<PsiReference> =
PsiReferenceService.getService().getContributedReferences(element).toList()
}