pork: it's got it all, ffi, state machine tokenizer, and better IDE support

This commit is contained in:
2023-10-13 01:04:35 -07:00
parent d355fb3914
commit 5078f38f61
58 changed files with 939 additions and 293 deletions

View File

@ -8,13 +8,14 @@ import com.intellij.platform.backend.navigation.NavigationRequest
import com.intellij.platform.backend.navigation.NavigationTarget
import com.intellij.platform.backend.presentation.TargetPresentation
import gay.pizza.pork.idea.psi.gen.PorkElement
import gay.pizza.pork.idea.resolution.PorkReferenceResolution
@Suppress("UnstableApiUsage")
data class PorkDeclarationSymbol(val module: String, val name: String) : Symbol, NavigatableSymbol {
override fun createPointer(): Pointer<out Symbol> = Pointer { this }
override fun getNavigationTargets(project: Project): MutableCollection<out NavigationTarget> {
return PorkReferenceResolution.getAllProjectPorkFiles(project)
.flatMap { PorkReferenceResolution.findAnyDefinitions(it) }
.flatMap { PorkReferenceResolution.findAnyDefinitions(it.file) }
.map { PorkNavigationTarget(it) }
.toMutableList()
}

View File

@ -35,6 +35,10 @@ object PorkElementTypes {
elementTypeFor(TokenType.StringLiteral)
)
val QuoteSet = TokenSet.create(
elementTypeFor(TokenType.Quote)
)
fun tokenTypeFor(elementType: IElementType): TokenType? =
elementTypeToTokenType[elementType]

View File

@ -10,4 +10,8 @@ class PorkFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, PorkL
}
override fun toString(): String = "Pork"
override fun isPhysical(): Boolean {
return super.isPhysical()
}
}

View File

@ -0,0 +1,34 @@
package gay.pizza.pork.idea
import com.intellij.codeInsight.hints.InlayInfo
import com.intellij.codeInsight.hints.InlayParameterHintsProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.util.childrenOfType
import gay.pizza.pork.idea.psi.gen.ArgumentSpecElement
import gay.pizza.pork.idea.psi.gen.FunctionDefinitionElement
@Suppress("UnstableApiUsage")
class PorkInlayParameterHintsProvider : InlayParameterHintsProvider {
override fun getParameterHints(element: PsiElement): MutableList<InlayInfo> {
val inlays = mutableListOf<InlayInfo>()
val resolved = element.reference?.resolve()
if (resolved !is FunctionDefinitionElement) {
return inlays
}
val argumentSpecs = resolved.childrenOfType<ArgumentSpecElement>()
val arguments = if (element.children.isNotEmpty()) {
element.children.drop(1)
} else emptyList()
for ((argument, spec) in arguments.zip(argumentSpecs)) {
val name = spec.name
if (name != null) {
inlays.add(InlayInfo(name, argument.textOffset))
}
}
return inlays
}
override fun getDefaultBlackList(): MutableSet<String> =
mutableSetOf()
}

View File

@ -0,0 +1,90 @@
package gay.pizza.pork.idea
import com.intellij.lang.parameterInfo.CreateParameterInfoContext
import com.intellij.lang.parameterInfo.ParameterInfoHandler
import com.intellij.lang.parameterInfo.ParameterInfoUIContext
import com.intellij.lang.parameterInfo.ParameterInfoUtils
import com.intellij.lang.parameterInfo.UpdateParameterInfoContext
import com.intellij.openapi.util.TextRange
import com.intellij.psi.util.childrenOfType
import com.intellij.psi.util.elementsAtOffsetUp
import gay.pizza.pork.idea.psi.gen.ArgumentSpecElement
import gay.pizza.pork.idea.psi.gen.FunctionCallElement
import gay.pizza.pork.idea.psi.gen.FunctionDefinitionElement
import gay.pizza.pork.parser.TokenType
@Suppress("UnstableApiUsage")
class PorkParameterInfoHandler : ParameterInfoHandler<FunctionCallElement, FunctionDefinitionElement> {
override fun findElementForParameterInfo(context: CreateParameterInfoContext): FunctionCallElement? {
return context.file.elementsAtOffsetUp(context.offset).asSequence()
.map { it.first }
.filterIsInstance<FunctionCallElement>()
.firstOrNull()
}
override fun findElementForUpdatingParameterInfo(context: UpdateParameterInfoContext): FunctionCallElement? {
return context.file.elementsAtOffsetUp(context.offset).asSequence()
.map { it.first }
.filterIsInstance<FunctionCallElement>()
.firstOrNull()
}
override fun updateUI(p: FunctionDefinitionElement, context: ParameterInfoUIContext) {
val argumentSpecs = p.childrenOfType<ArgumentSpecElement>()
val signature = argumentSpecs.mapNotNull { it.name }.joinToString(", ")
if (argumentSpecs.isEmpty()) {
context.setupUIComponentPresentation(
"<no parameters>",
-1,
-1,
false,
false,
false,
context.defaultParameterColor
)
return
}
if (context.currentParameterIndex >= argumentSpecs.size) {
context.setupUIComponentPresentation(
signature,
-1,
-1,
false,
false,
false,
context.defaultParameterColor
)
} else {
var range: TextRange? = null
var start = 0
for ((index, item) in signature.split(", ").withIndex()) {
if (index == context.currentParameterIndex) {
range = TextRange(index, index + item.length)
}
start += item.length + 2
}
context.setupUIComponentPresentation(
signature,
range?.startOffset ?: 0,
range?.endOffset ?: (signature.length - 1),
false,
false,
false,
context.defaultParameterColor
)
}
}
override fun updateParameterInfo(parameterOwner: FunctionCallElement, context: UpdateParameterInfoContext) {
val offset = ParameterInfoUtils.getCurrentParameterIndex(
parameterOwner.node,
context.offset,
PorkElementTypes.elementTypeFor(TokenType.Comma)
)
context.setCurrentParameter(offset)
}
override fun showParameterInfo(element: FunctionCallElement, context: CreateParameterInfoContext) {
context.showHint(element, element.textOffset, this)
}
}

View File

@ -0,0 +1,5 @@
package gay.pizza.pork.idea
import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler
class PorkQuoteHandler : SimpleTokenSetQuoteHandler(PorkElementTypes.QuoteSet)

View File

@ -1,109 +0,0 @@
package gay.pizza.pork.idea
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.childrenOfType
import gay.pizza.pork.idea.psi.gen.*
object PorkReferenceResolution {
fun getRelevantFiles(containingFile: PsiFile): List<PsiFile> {
if (containingFile.virtualFile == null) {
return getAllProjectPorkFiles(containingFile.project)
}
val importDeclarationElements = PsiTreeUtil.collectElementsOfType(containingFile, ImportDeclarationElement::class.java)
val files = mutableListOf(containingFile)
for (importDeclaration in importDeclarationElements) {
val symbolElements = importDeclaration.childrenOfType<SymbolElement>()
val importType = importDeclaration.childrenOfType<SymbolElement>().first().text
if (importType != "local") {
continue
}
val basicImportPath = symbolElements.drop(1).joinToString("/") { it.text.trim() }
val actualImportPath = "../${basicImportPath}.pork"
val virtualFile = containingFile.virtualFile?.findFileByRelativePath(actualImportPath) ?: continue
val psiFile = PsiManager.getInstance(containingFile.project).findFile(virtualFile) ?: continue
files.add(psiFile)
}
return files
}
fun getAllProjectPorkFiles(project: Project): List<PsiFile> {
val porkVirtualFiles = FilenameIndex.getAllFilesByExt(project, "pork")
return porkVirtualFiles.mapNotNull { virtualFile ->
PsiManager.getInstance(project).findFile(virtualFile)
}
}
fun findAllCandidates(internalPorkElement: PorkElement, name: String? = null): List<PorkElement> =
listOf(findAnyLocals(internalPorkElement, name), findAnyDefinitions(internalPorkElement.containingFile, name)).flatten()
fun findAnyLocals(internalPorkElement: PorkElement, name: String? = null): List<PorkElement> {
val functionDefinitionElement = PsiTreeUtil.getParentOfType(internalPorkElement, FunctionDefinitionElement::class.java)
?: return emptyList()
val locals = mutableListOf<PorkElement>()
fun check(localCandidate: PsiElement, upward: Boolean) {
if (localCandidate is BlockElement && !upward) {
return
}
if (localCandidate is ArgumentSpecElement ||
localCandidate is LetAssignmentElement ||
localCandidate is VarAssignmentElement) {
locals.add(localCandidate as PorkElement)
}
if (localCandidate is ForInElement) {
val forInItem = localCandidate.childrenOfType<ForInItemElement>().firstOrNull()
if (forInItem != null) {
locals.add(forInItem)
}
}
localCandidate.children.forEach { check(it, false) }
}
PsiTreeUtil.treeWalkUp(internalPorkElement, functionDefinitionElement) { _, localCandidate ->
if (localCandidate != null) {
if (internalPorkElement == functionDefinitionElement) {
return@treeWalkUp true
}
check(localCandidate, true)
}
true
}
val argumentSpecElements = functionDefinitionElement.childrenOfType<ArgumentSpecElement>()
locals.addAll(argumentSpecElements)
val finalLocals = locals.distinctBy { it.textRange }
return finalLocals.filter { if (name != null) it.name == name else true }
}
fun findAnyDefinitions(containingFile: PsiFile, name: String? = null): List<PorkElement> {
val foundDefinitions = mutableListOf<PorkNamedElement>()
for (file in getRelevantFiles(containingFile)) {
val definitions = PsiTreeUtil.collectElements(file) { element ->
element is FunctionDefinitionElement ||
element is LetDefinitionElement
}.filterIsInstance<PorkNamedElement>()
if (name != null) {
val fileFoundDefinition = definitions.firstOrNull {
it.name == name
}
if (fileFoundDefinition != null) {
foundDefinitions.add(fileFoundDefinition)
return foundDefinitions
}
} else {
foundDefinitions.addAll(definitions)
}
}
return foundDefinitions
}
}

View File

@ -49,7 +49,13 @@ object PorkElementHelpers {
fun referenceOfElement(element: PorkElement, type: NodeType): PsiReference? {
unused(type)
val textRangeOfSymbolInElement = element.childrenOfType<SymbolElement>().firstOrNull()?.textRangeInParent ?: return null
if (element is ImportPathElement) {
return PorkFileReference(element, element.textRange)
}
val symbols = element.childrenOfType<SymbolElement>()
val textRangeOfSymbolInElement = symbols.firstOrNull()?.textRangeInParent ?: return null
return PorkIdentifierReference(element, textRangeOfSymbolInElement)
}

View File

@ -0,0 +1,22 @@
package gay.pizza.pork.idea.psi
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.util.parentOfType
import gay.pizza.pork.idea.psi.gen.ImportDeclarationElement
import gay.pizza.pork.idea.psi.gen.PorkElement
import gay.pizza.pork.idea.resolution.PorkReferenceResolution
class PorkFileReference(element: PorkElement, textRange: TextRange) : PorkReference(element, textRange) {
override fun resolve(): PsiElement? {
val importDeclarationElement = element.parentOfType<ImportDeclarationElement>() ?: return null
val resolved = PorkReferenceResolution.resolveImportFile(
element.containingFile,
PorkReferenceResolution.findPorkStdDirectory(element.project),
importDeclarationElement
)
return resolved?.file
}
override fun getVariants(): Array<Any> = arrayOf()
}

View File

@ -1,15 +1,17 @@
package gay.pizza.pork.idea.psi
import com.intellij.psi.PsiFile
import gay.pizza.pork.idea.PorkReferenceResolution
import gay.pizza.pork.idea.resolution.PorkReferenceResolution
import gay.pizza.pork.idea.psi.gen.PorkElement
import gay.pizza.pork.idea.resolution.PorkReferenceRelevantFile
interface PorkReferencable {
val internalPorkElement: PorkElement
fun getRelevantFiles(): List<PsiFile> = PorkReferenceResolution.getRelevantFiles(internalPorkElement.containingFile)
fun getRelevantFiles(): List<PorkReferenceRelevantFile> =
PorkReferenceResolution.getRelevantFiles(internalPorkElement.containingFile)
fun findAllCandidates(name: String? = null): List<PorkElement> =
listOf(findAnyLocals(name), findAnyDefinitions(name)).flatten()
PorkReferenceResolution.findAllCandidates(internalPorkElement, name)
fun findAnyLocals(name: String? = null): List<PorkElement> =
PorkReferenceResolution.findAnyLocals(internalPorkElement, name)

View File

@ -0,0 +1,20 @@
// GENERATED CODE FROM PORK AST CODEGEN
package gay.pizza.pork.idea.psi.gen
import com.intellij.lang.ASTNode
import com.intellij.navigation.ItemPresentation
import com.intellij.psi.PsiReference
import gay.pizza.pork.ast.gen.NodeType
import gay.pizza.pork.idea.psi.PorkElementHelpers
import javax.swing.Icon
class ImportPathElement(node: ASTNode) : PorkElement(node) {
override fun getReference(): PsiReference? =
PorkElementHelpers.referenceOfElement(this, NodeType.CompilationUnit)
override fun getIcon(flags: Int): Icon? =
PorkElementHelpers.iconOf(this)
override fun getPresentation(): ItemPresentation? =
PorkElementHelpers.presentationOf(this)
}

View File

@ -23,6 +23,7 @@ object PorkElementFactory {
NodeType.FunctionDefinition -> FunctionDefinitionElement(node)
NodeType.LetDefinition -> LetDefinitionElement(node)
NodeType.If -> IfElement(node)
NodeType.ImportPath -> ImportPathElement(node)
NodeType.ImportDeclaration -> ImportDeclarationElement(node)
NodeType.IntegerLiteral -> IntegerLiteralElement(node)
NodeType.LongLiteral -> LongLiteralElement(node)

View File

@ -0,0 +1,5 @@
package gay.pizza.pork.idea.resolution
import com.intellij.psi.PsiFile
class PorkReferenceRelevantFile(val file: PsiFile, val type: PorkRelevantFileType)

View File

@ -0,0 +1,197 @@
package gay.pizza.pork.idea.resolution
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.vfs.*
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.childrenOfType
import gay.pizza.pork.idea.psi.gen.*
object PorkReferenceResolution {
fun getRelevantFiles(containingFile: PsiFile): List<PorkReferenceRelevantFile> {
if (containingFile.virtualFile == null) {
return listOf(
getAllProjectPorkFiles(containingFile.project),
getAllPorkStdFiles(containingFile.project)
).flatten()
}
val importDeclarationElements = PsiTreeUtil.collectElementsOfType(
containingFile,
ImportDeclarationElement::class.java
)
val files = mutableListOf(PorkReferenceRelevantFile(containingFile, PorkRelevantFileType.Self))
val stdDirectory = findPorkStdDirectory(containingFile.project)
val prelude = resolveStdImport(containingFile, stdDirectory, "lang/prelude")
if (prelude != null) {
files.add(prelude)
}
for (importDeclaration in importDeclarationElements) {
val resolved = resolveImportFile(containingFile, stdDirectory, importDeclaration)
if (resolved != null) {
files.add(resolved)
}
}
return files
}
fun resolveImportFile(
containingFile: PsiFile,
stdDirectory: VirtualFile?,
importDeclarationElement: ImportDeclarationElement
): PorkReferenceRelevantFile? {
val importType = importDeclarationElement.childrenOfType<SymbolElement>().firstOrNull()?.text ?: return null
val importPathElement = importDeclarationElement.childrenOfType<ImportPathElement>().firstOrNull() ?: return null
val basicImportPath = importPathElement.children.joinToString("/") { it.text.trim() }
return when (importType) {
"local" -> {
val actualImportPath = "../${basicImportPath}.pork"
val actualVirtualFile = containingFile.virtualFile?.findFileByRelativePath(actualImportPath) ?: return null
referenceRelevantFile(containingFile.project, actualVirtualFile, PorkRelevantFileType.Local)
}
"std" -> {
resolveStdImport(containingFile, stdDirectory, basicImportPath)
}
else -> null
}
}
private fun resolveStdImport(containingFile: PsiFile, stdDirectory: VirtualFile?, basicImportPath: String): PorkReferenceRelevantFile? {
if (stdDirectory == null) {
return null
}
val actualVirtualFile = stdDirectory.findFile("${basicImportPath}.pork") ?: return null
return referenceRelevantFile(containingFile.project, actualVirtualFile, PorkRelevantFileType.Std)
}
private fun referenceRelevantFile(
project: Project,
virtualFile: VirtualFile,
type: PorkRelevantFileType
): PorkReferenceRelevantFile? {
val psiFile = PsiManager.getInstance(project).findFile(virtualFile) ?: return null
return PorkReferenceRelevantFile(psiFile, type)
}
fun getAllProjectPorkFiles(project: Project): List<PorkReferenceRelevantFile> {
val psiManager = PsiManager.getInstance(project)
val porkVirtualFiles = FilenameIndex.getAllFilesByExt(project, "pork")
return porkVirtualFiles.mapNotNull { virtualFile ->
psiManager.findFile(virtualFile)
}.map { PorkReferenceRelevantFile(it, PorkRelevantFileType.Local) }
}
fun findPorkStdDirectory(project: Project): VirtualFile? = if (isPorkItself(project)) {
project.guessProjectDir()?.findDirectory("stdlib/src/main/pork")
} else {
project.guessProjectDir()?.fileSystem?.findFileByPath(
"/opt/pork/std"
)
}
fun getAllPorkStdFiles(project: Project): List<PorkReferenceRelevantFile> {
val stdDirectoryPath = findPorkStdDirectory(project) ?: return emptyList()
val psiManager = PsiManager.getInstance(project)
val stdPorkFiles = mutableListOf<PorkReferenceRelevantFile>()
VfsUtilCore.iterateChildrenRecursively(stdDirectoryPath, VirtualFileFilter.ALL) { file ->
if (file.extension == "pork") {
val psiFile = psiManager.findFile(file)
if (psiFile != null) {
stdPorkFiles.add(PorkReferenceRelevantFile(psiFile, PorkRelevantFileType.Std))
}
}
true
}
return stdPorkFiles
}
private fun isPorkItself(project: Project): Boolean {
if (project.name != "pork") {
return false
}
val projectDirectory = project.guessProjectDir() ?: return false
val prelude = projectDirectory.findFileOrDirectory(
"stdlib/src/main/pork/lang/prelude.pork"
)
return prelude != null && prelude.isFile
}
fun findAllCandidates(internalPorkElement: PorkElement, name: String? = null): List<PorkElement> =
listOf(
findAnyLocals(internalPorkElement, name),
findAnyDefinitions(internalPorkElement.containingFile, name)
).flatten()
fun findAnyLocals(internalPorkElement: PorkElement, name: String? = null): List<PorkElement> {
val functionDefinitionElement = PsiTreeUtil.getParentOfType(
internalPorkElement,
FunctionDefinitionElement::class.java
) ?: return emptyList()
val locals = mutableListOf<PorkElement>()
fun check(localCandidate: PsiElement, upward: Boolean) {
if (localCandidate is BlockElement && !upward) {
return
}
if (localCandidate is ArgumentSpecElement ||
localCandidate is LetAssignmentElement ||
localCandidate is VarAssignmentElement) {
locals.add(localCandidate as PorkElement)
}
if (localCandidate is ForInElement) {
val forInItem = localCandidate.childrenOfType<ForInItemElement>().firstOrNull()
if (forInItem != null) {
locals.add(forInItem)
}
}
localCandidate.children.forEach { check(it, false) }
}
PsiTreeUtil.treeWalkUp(internalPorkElement, functionDefinitionElement) { _, localCandidate ->
if (localCandidate != null) {
if (internalPorkElement == functionDefinitionElement) {
return@treeWalkUp true
}
check(localCandidate, true)
}
true
}
val argumentSpecElements = functionDefinitionElement.childrenOfType<ArgumentSpecElement>()
locals.addAll(argumentSpecElements)
val finalLocals = locals.distinctBy { it.textRange }
return finalLocals.filter { if (name != null) it.name == name else true }
}
fun findAnyDefinitions(containingFile: PsiFile, name: String? = null): List<PorkElement> {
val foundDefinitions = mutableListOf<PorkNamedElement>()
for (file in getRelevantFiles(containingFile)) {
val definitions = PsiTreeUtil.collectElements(file.file) { element ->
element is FunctionDefinitionElement ||
element is LetDefinitionElement
}.filterIsInstance<PorkNamedElement>()
if (name != null) {
val fileFoundDefinition = definitions.firstOrNull {
it.name == name
}
if (fileFoundDefinition != null) {
foundDefinitions.add(fileFoundDefinition)
return foundDefinitions
}
} else {
foundDefinitions.addAll(definitions)
}
}
return foundDefinitions
}
}

View File

@ -0,0 +1,7 @@
package gay.pizza.pork.idea.resolution
enum class PorkRelevantFileType {
Self,
Local,
Std
}

View File

@ -26,6 +26,15 @@
<lang.elementManipulator
implementationClass="gay.pizza.pork.idea.PorkElementManipulator"
forClass="gay.pizza.pork.idea.psi.gen.PorkElement"/>
<codeInsight.parameterNameHints
language="Pork"
implementationClass="gay.pizza.pork.idea.PorkInlayParameterHintsProvider"/>
<lang.quoteHandler
language="Pork"
implementationClass="gay.pizza.pork.idea.PorkQuoteHandler"/>
<!-- <codeInsight.parameterInfo
language="Pork"
implementationClass="gay.pizza.pork.idea.PorkParameterInfoHandler"/>-->
<psi.declarationProvider implementation="gay.pizza.pork.idea.PorkSymbolDeclarationProvider"/>
</extensions>