Phase 4: add structure, folding, and references
This commit is contained in:
@@ -3,6 +3,8 @@ package dev.hgh.jai.editor
|
||||
import com.intellij.codeInsight.highlighting.BraceMatchingUtil
|
||||
import com.intellij.lang.LanguageBraceMatching
|
||||
import com.intellij.lang.LanguageCommenters
|
||||
import com.intellij.lang.LanguageStructureViewBuilder
|
||||
import com.intellij.lang.folding.LanguageFolding
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import dev.hgh.jai.JaiFileType
|
||||
@@ -22,6 +24,24 @@ class JaiEditorSupportTest : BasePlatformTestCase() {
|
||||
assertTrue("expected JaiBraceMatcher, got $matcher", matcher is JaiBraceMatcher)
|
||||
}
|
||||
|
||||
fun testFoldingBuilderIsRegistered() {
|
||||
val foldingBuilders = LanguageFolding.INSTANCE.allForLanguage(JaiLanguage)
|
||||
assertTrue(
|
||||
"expected JaiFoldingBuilder in $foldingBuilders",
|
||||
foldingBuilders.any { it is JaiFoldingBuilder },
|
||||
)
|
||||
}
|
||||
|
||||
fun testStructureViewFactoryIsRegistered() {
|
||||
val file = myFixture.configureByText("structure.jai", "value := 1;")
|
||||
val builder = LanguageStructureViewBuilder.getInstance().getStructureViewBuilder(file)
|
||||
assertNotNull("expected a Jai structure-view builder", builder)
|
||||
assertTrue(
|
||||
"expected a TreeBasedStructureViewBuilder, got $builder",
|
||||
builder is com.intellij.ide.structureView.TreeBasedStructureViewBuilder,
|
||||
)
|
||||
}
|
||||
|
||||
fun testMatchesOrdinaryBraces() {
|
||||
assertMatchForward("main :: () |{ x := 1; }", "main :: () { x := 1; |}")
|
||||
}
|
||||
|
||||
65
src/test/kotlin/dev/hgh/jai/editor/JaiFoldingBuilderTest.kt
Normal file
65
src/test/kotlin/dev/hgh/jai/editor/JaiFoldingBuilderTest.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
package dev.hgh.jai.editor
|
||||
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import dev.hgh.jai.lexer.JaiTokenTypes
|
||||
|
||||
class JaiFoldingBuilderTest : BasePlatformTestCase() {
|
||||
fun testFoldsMultilineBlocksAndComments() {
|
||||
val text =
|
||||
"""
|
||||
main :: () {
|
||||
value := 1;
|
||||
}
|
||||
|
||||
/* a comment
|
||||
* with two lines
|
||||
*/
|
||||
""".trimIndent()
|
||||
val file = myFixture.configureByText("folding.jai", text)
|
||||
|
||||
val descriptors = JaiFoldingBuilder().buildFoldRegions(file, myFixture.editor.document, quick = false)
|
||||
val foldedText =
|
||||
descriptors.map { descriptor ->
|
||||
text.substring(descriptor.range.startOffset, descriptor.range.endOffset)
|
||||
}
|
||||
|
||||
assertEquals(2, descriptors.size)
|
||||
assertTrue(foldedText.any { it.contains("value := 1;") })
|
||||
assertTrue(foldedText.any { it.contains("with two lines") })
|
||||
}
|
||||
|
||||
fun testFoldsNestedCommentsAndHereStrings() {
|
||||
val text =
|
||||
"""
|
||||
/* outer comment
|
||||
/* nested comment */
|
||||
still in the outer comment
|
||||
*/
|
||||
body :: #string END
|
||||
raw here-string text
|
||||
END;
|
||||
""".trimIndent()
|
||||
val file = myFixture.configureByText("literals.jai", text)
|
||||
val builder = JaiFoldingBuilder()
|
||||
val descriptors = builder.buildFoldRegions(file, myFixture.editor.document, quick = false)
|
||||
val foldedText =
|
||||
descriptors.map { descriptor ->
|
||||
text.substring(descriptor.range.startOffset, descriptor.range.endOffset)
|
||||
}
|
||||
|
||||
assertEquals(2, descriptors.size)
|
||||
assertTrue(foldedText.any { it.contains("nested comment") })
|
||||
assertTrue(foldedText.any { it.contains("raw here-string text") })
|
||||
val hereString = descriptors.first { it.element.elementType == JaiTokenTypes.HERE_STRING }
|
||||
assertEquals("#string …", builder.getPlaceholderText(hereString.element))
|
||||
}
|
||||
|
||||
fun testDoesNotFoldSingleLineRegions() {
|
||||
val text = "main :: () { value := 1; } /* one line */"
|
||||
val file = myFixture.configureByText("single-line.jai", text)
|
||||
|
||||
val descriptors = JaiFoldingBuilder().buildFoldRegions(file, myFixture.editor.document, quick = false)
|
||||
|
||||
assertEmpty(descriptors.toList())
|
||||
}
|
||||
}
|
||||
101
src/test/kotlin/dev/hgh/jai/reference/JaiReferenceTest.kt
Normal file
101
src/test/kotlin/dev/hgh/jai/reference/JaiReferenceTest.kt
Normal file
@@ -0,0 +1,101 @@
|
||||
package dev.hgh.jai.reference
|
||||
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.psi.PsiReference
|
||||
import com.intellij.psi.PsiReferenceService
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import dev.hgh.jai.psi.JaiLiteralExpr
|
||||
|
||||
class JaiReferenceTest : BasePlatformTestCase() {
|
||||
fun testLoadResolvesAFileRelativeToTheSource() {
|
||||
val target = myFixture.addFileToProject("other.jai", "value := 1;")
|
||||
val source = myFixture.addFileToProject("main.jai", "#load \"other.jai\";")
|
||||
val literal = literalWithText(source, "\"other.jai\"")
|
||||
|
||||
val reference = onlyReference(literal)
|
||||
val resolved = reference.resolve() as? PsiFile
|
||||
|
||||
assertNotNull("#load should resolve its sibling file", resolved)
|
||||
assertEquals(target.virtualFile.path, resolved!!.virtualFile.path)
|
||||
assertEquals(TextRange(1, literal.textLength - 1), reference.rangeInElement)
|
||||
}
|
||||
|
||||
fun testImportResolvesAJaiModuleDirectory() {
|
||||
val sourceVirtualFile =
|
||||
LocalFileSystem
|
||||
.getInstance()
|
||||
.findFileByPath("${System.getProperty("user.home")}/.local/jai/how_to/001_first.jai")
|
||||
assertNotNull("expected the local Jai corpus", sourceVirtualFile)
|
||||
val source = PsiManager.getInstance(project).findFile(sourceVirtualFile!!)
|
||||
assertNotNull("expected PSI for ${sourceVirtualFile.path}", source)
|
||||
val literal =
|
||||
PsiTreeUtil
|
||||
.findChildrenOfType(source, JaiLiteralExpr::class.java)
|
||||
.firstOrNull { it.text == "\"Basic\"" }
|
||||
assertNotNull("expected #import \"Basic\" in ${sourceVirtualFile.path}", literal)
|
||||
|
||||
val resolved = onlyReference(literal!!).resolve() as? PsiFile
|
||||
val expectedPath = "${System.getProperty("user.home")}/.local/jai/modules/Basic/module.jai"
|
||||
|
||||
assertNotNull("#import should resolve a Jai module directory", resolved)
|
||||
assertEquals(expectedPath, resolved!!.virtualFile.path)
|
||||
}
|
||||
|
||||
fun testExplicitImportModesUseTheContainingDirectory() {
|
||||
val source = localPsiFile("how_to/040_import_and_load/main.jai")
|
||||
val fileReference = onlyReference(literalWithText(source, "\"files/specific.jai\""))
|
||||
val directoryReference = onlyReference(literalWithText(source, "\"files/From_Subdirectory\""))
|
||||
|
||||
assertEquals(
|
||||
"${System.getProperty("user.home")}/.local/jai/how_to/040_import_and_load/files/specific.jai",
|
||||
(fileReference.resolve() as PsiFile).virtualFile.path,
|
||||
)
|
||||
assertEquals(
|
||||
"${System.getProperty("user.home")}/.local/jai/how_to/040_import_and_load/files/From_Subdirectory/module.jai",
|
||||
(directoryReference.resolve() as PsiFile).virtualFile.path,
|
||||
)
|
||||
}
|
||||
|
||||
fun testImportStringIsNotTreatedAsAFileReference() {
|
||||
val source = myFixture.addFileToProject("main.jai", "#import,string \"inline code\";")
|
||||
val literal = literalWithText(source, "\"inline code\"")
|
||||
|
||||
assertTrue(references(literal).isEmpty())
|
||||
}
|
||||
|
||||
private fun localPsiFile(relativePath: String): PsiFile {
|
||||
val virtualFile =
|
||||
LocalFileSystem
|
||||
.getInstance()
|
||||
.findFileByPath("${System.getProperty("user.home")}/.local/jai/$relativePath")
|
||||
assertNotNull("expected the local Jai corpus", virtualFile)
|
||||
val psiFile = PsiManager.getInstance(project).findFile(virtualFile!!)
|
||||
assertNotNull("expected PSI for ${virtualFile.path}", psiFile)
|
||||
return psiFile!!
|
||||
}
|
||||
|
||||
private fun literalWithText(
|
||||
source: PsiFile,
|
||||
text: String,
|
||||
): JaiLiteralExpr {
|
||||
val literal =
|
||||
PsiTreeUtil
|
||||
.findChildrenOfType(source, JaiLiteralExpr::class.java)
|
||||
.firstOrNull { it.text == text }
|
||||
assertNotNull("expected string literal $text in ${source.virtualFile.path}", literal)
|
||||
return literal!!
|
||||
}
|
||||
|
||||
private fun onlyReference(literal: JaiLiteralExpr): PsiReference {
|
||||
val references = references(literal)
|
||||
assertEquals("expected exactly one path reference", 1, references.size)
|
||||
return references.single()
|
||||
}
|
||||
|
||||
private fun references(literal: JaiLiteralExpr): List<PsiReference> =
|
||||
PsiReferenceService.getService().getContributedReferences(literal).toList()
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package dev.hgh.jai.structure
|
||||
|
||||
import com.intellij.ide.structureView.StructureViewTreeElement
|
||||
import com.intellij.ide.structureView.TreeBasedStructureViewBuilder
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
|
||||
class JaiStructureViewTest : BasePlatformTestCase() {
|
||||
fun testListsTopLevelAndNestedDeclarationsInSourceOrder() {
|
||||
val file =
|
||||
myFixture.configureByText(
|
||||
"structure.jai",
|
||||
"""
|
||||
main :: () {
|
||||
helper :: () {}
|
||||
}
|
||||
Point :: struct {
|
||||
x: int;
|
||||
}
|
||||
operator + :: (a: Point, b: Point) -> Point {}
|
||||
""".trimIndent(),
|
||||
)
|
||||
val model = JaiStructureViewModel(null, file)
|
||||
val root: StructureViewTreeElement = model.getRoot()
|
||||
|
||||
assertEquals(listOf("main", "Point", "operator +"), children(root).map(::label))
|
||||
val main = children(root)[0]
|
||||
assertEquals(listOf("helper"), children(main).map(::label))
|
||||
assertEquals(0, (main.getValue() as PsiElement).textOffset)
|
||||
}
|
||||
|
||||
fun testOnlyDirectDeclarationsBecomeChildren() {
|
||||
val file =
|
||||
myFixture.configureByText(
|
||||
"nested.jai",
|
||||
"""
|
||||
main :: () {
|
||||
if true {
|
||||
hidden :: () {}
|
||||
}
|
||||
visible :: () {}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
val root: StructureViewTreeElement = JaiStructureViewModel(null, file).getRoot()
|
||||
|
||||
assertEquals(listOf("visible"), children(children(root)[0]).map(::label))
|
||||
}
|
||||
|
||||
fun testFactoryCreatesAUsableStructureModel() {
|
||||
val file = myFixture.configureByText("empty.jai", "value := 1;")
|
||||
val builder = JaiStructureViewFactory().getStructureViewBuilder(file) as TreeBasedStructureViewBuilder
|
||||
val model = builder.createStructureViewModel(null)
|
||||
val root: StructureViewTreeElement = model.getRoot()
|
||||
|
||||
assertEquals("empty.jai", label(root))
|
||||
assertEquals(listOf("value"), children(root).map(::label))
|
||||
}
|
||||
|
||||
private fun children(element: StructureViewTreeElement): List<StructureViewTreeElement> =
|
||||
element.getChildren().map { it as StructureViewTreeElement }
|
||||
|
||||
private fun label(element: StructureViewTreeElement): String = element.getPresentation().presentableText ?: ""
|
||||
}
|
||||
Reference in New Issue
Block a user