idea: enhance symbol support

This commit is contained in:
Alex Zenla 2023-09-23 18:51:02 -07:00
parent 87623505c0
commit 97283636bc
Signed by: alex
GPG Key ID: C0780728420EBFE5
10 changed files with 245 additions and 106 deletions

View File

@ -0,0 +1,39 @@
package gay.pizza.pork.idea
import com.intellij.model.Pointer
import com.intellij.model.Symbol
import com.intellij.navigation.ItemPresentation
import com.intellij.navigation.NavigatableSymbol
import com.intellij.navigation.PsiElementNavigationItem
import com.intellij.openapi.project.Project
import com.intellij.platform.backend.navigation.NavigationRequest
import com.intellij.platform.backend.navigation.NavigationRequests
import com.intellij.platform.backend.navigation.NavigationTarget
import com.intellij.platform.backend.presentation.TargetPresentation
import com.intellij.psi.PsiElement
import gay.pizza.pork.idea.psi.PorkElementHelpers
import gay.pizza.pork.idea.psi.PorkReferencable
import gay.pizza.pork.idea.psi.gen.PorkElement
@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) }
.map { PorkNavigationTarget(it) }
.toMutableList()
}
class PorkNavigationTarget(val internalPorkElement: PorkElement) : NavigationTarget {
override fun createPointer(): Pointer<out NavigationTarget> = Pointer { this }
override fun computePresentation(): TargetPresentation = TargetPresentation
.builder(internalPorkElement.name!!)
.presentation()
override fun navigationRequest(): NavigationRequest? {
return NavigationRequest.sourceNavigationRequest(internalPorkElement.containingFile, internalPorkElement.textRange)
}
}
}

View File

@ -0,0 +1,109 @@
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

@ -0,0 +1,21 @@
package gay.pizza.pork.idea
import com.intellij.model.Symbol
import com.intellij.model.psi.PsiSymbolDeclaration
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import gay.pizza.pork.idea.psi.PorkElementHelpers
import gay.pizza.pork.idea.psi.gen.PorkElement
@Suppress("UnstableApiUsage")
class PorkSymbolDeclaration(val element: PorkElement) : PsiSymbolDeclaration {
override fun getDeclaringElement(): PsiElement = element
override fun getRangeInDeclaringElement(): TextRange {
val textRangeOfSymbol = PorkElementHelpers.symbolElementOf(element)?.psi?.textRangeInParent
?: throw RuntimeException("Unable to get symbol of element: $element")
return textRangeOfSymbol
}
override fun getSymbol(): Symbol = PorkElementHelpers.psiSymbolFor(element) ?:
throw RuntimeException("Unable to get symbol of element: $element")
}

View File

@ -0,0 +1,19 @@
package gay.pizza.pork.idea
import com.intellij.model.psi.PsiSymbolDeclaration
import com.intellij.model.psi.PsiSymbolDeclarationProvider
import com.intellij.psi.PsiElement
import gay.pizza.pork.idea.psi.gen.FunctionDefinitionElement
import gay.pizza.pork.idea.psi.gen.LetDefinitionElement
import gay.pizza.pork.idea.psi.gen.PorkElement
@Suppress("UnstableApiUsage")
class PorkSymbolDeclarationProvider : PsiSymbolDeclarationProvider {
override fun getDeclarations(element: PsiElement, offsetInElement: Int): MutableCollection<out PsiSymbolDeclaration> {
if (element !is PorkElement) return mutableListOf()
if (element is FunctionDefinitionElement || element is LetDefinitionElement) {
return mutableListOf(PorkSymbolDeclaration(element))
}
return mutableListOf()
}
}

View File

@ -0,0 +1,18 @@
package gay.pizza.pork.idea
import com.intellij.model.Symbol
import com.intellij.model.psi.PsiSymbolReference
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import gay.pizza.pork.idea.psi.PorkElementHelpers
import gay.pizza.pork.idea.psi.PorkReferencable
import gay.pizza.pork.idea.psi.gen.PorkElement
@Suppress("UnstableApiUsage")
class PorkSymbolReference(override val internalPorkElement: PorkElement, val range: TextRange) : PsiSymbolReference, PorkReferencable {
override fun getElement(): PsiElement = internalPorkElement
override fun getRangeInElement(): TextRange = range
override fun resolveReference(): MutableCollection<out Symbol> {
return findAllCandidates().mapNotNull { PorkElementHelpers.psiSymbolFor(it) }.toMutableList()
}
}

View File

@ -1,6 +1,7 @@
package gay.pizza.pork.idea.psi
import com.intellij.lang.ASTNode
import com.intellij.model.Symbol
import com.intellij.navigation.ItemPresentation
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileFactory
@ -11,28 +12,30 @@ import com.intellij.psi.util.childrenOfType
import com.intellij.util.PlatformIcons
import gay.pizza.pork.ast.NodeType
import gay.pizza.pork.common.unused
import gay.pizza.pork.idea.PorkDeclarationSymbol
import gay.pizza.pork.idea.PorkElementTypes
import gay.pizza.pork.idea.PorkLanguage
import gay.pizza.pork.idea.psi.gen.*
import javax.swing.Icon
@Suppress("UnstableApiUsage")
object PorkElementHelpers {
private val symbolElementType = PorkElementTypes.elementTypeFor(NodeType.Symbol)
fun nameOfNamedElement(element: PorkNamedElement): String? {
val child = symbolOf(element) ?: return null
val child = symbolElementOf(element) ?: return null
return child.text
}
fun setNameOfNamedElement(element: PorkNamedElement, name: String): PsiElement {
val child = symbolOf(element) ?: return element
val child = symbolElementOf(element) ?: return element
val factory = PsiFileFactory.getInstance(element.project) as PsiFileFactoryImpl
val created = factory.createElementFromText(name, PorkLanguage, child.elementType, element.context) as PorkElement
element.node.replaceChild(child, created.node)
return element
}
fun symbolOf(element: PorkElement): ASTNode? {
fun symbolElementOf(element: PorkElement): ASTNode? {
var child = element.node.findChildByType(symbolElementType)
if (child == null) {
child = PsiTreeUtil.collectElementsOfType(element, SymbolElement::class.java).firstOrNull()?.node
@ -41,7 +44,7 @@ object PorkElementHelpers {
}
fun nameIdentifierOfNamedElement(element: PorkNamedElement): PsiElement? {
return symbolOf(element)?.psi
return symbolElementOf(element)?.psi
}
fun referenceOfElement(element: PorkElement, type: NodeType): PsiReference? {
@ -73,4 +76,13 @@ object PorkElementHelpers {
return null
}
fun psiSymbolFor(element: PorkElement): Symbol? {
val symbolElement = symbolElementOf(element) ?: return null
val module = element.containingFile?.virtualFile?.path ?: element.containingFile?.name ?: return null
if (element is FunctionDefinitionElement || element is LetDefinitionElement) {
return PorkDeclarationSymbol(module, symbolElement.text)
}
return null
}
}

View File

@ -1,10 +1,7 @@
package gay.pizza.pork.idea.psi
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.childrenOfType
import gay.pizza.pork.idea.psi.gen.*
import gay.pizza.pork.idea.psi.gen.PorkElement
class PorkIdentifierReference(element: PorkElement, textRange: TextRange) : PorkReference(element, textRange) {
override fun resolve(): PorkElement? {
@ -20,72 +17,4 @@ class PorkIdentifierReference(element: PorkElement, textRange: TextRange) : Pork
val candidates = findAllCandidates()
return candidates.toTypedArray()
}
fun findAllCandidates(name: String? = null): List<PorkElement> =
listOf(findAnyLocals(name), findAnyDefinitions(name)).flatten()
fun findAnyLocals(name: String? = null): List<PorkElement> {
val functionDefinitionElement = PsiTreeUtil.getParentOfType(element, 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(element, functionDefinitionElement) { _, localCandidate ->
if (localCandidate != null) {
if (element == 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(name: String? = null): List<PorkElement> {
val foundDefinitions = mutableListOf<PorkNamedElement>()
for (file in getRelevantFiles()) {
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

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

View File

@ -12,34 +12,6 @@ import gay.pizza.pork.idea.psi.gen.ImportDeclarationElement
import gay.pizza.pork.idea.psi.gen.PorkElement
import gay.pizza.pork.idea.psi.gen.SymbolElement
abstract class PorkReference(element: PorkElement, textRange: TextRange) : PsiReferenceBase<PsiElement>(element, textRange) {
fun getRelevantFiles(): List<PsiFile> {
val containingFile = element.containingFile ?: return emptyList()
if (containingFile.virtualFile == null) {
return getAllProjectPorkFiles()
}
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(element.project).findFile(virtualFile) ?: continue
files.add(psiFile)
}
return files
}
fun getAllProjectPorkFiles(): List<PsiFile> {
val porkVirtualFiles = FilenameIndex.getAllFilesByExt(element.project, "pork")
return porkVirtualFiles.mapNotNull { virtualFile ->
PsiManager.getInstance(element.project).findFile(virtualFile)
}
}
abstract class PorkReference(element: PorkElement, textRange: TextRange) : PsiReferenceBase<PsiElement>(element, textRange), PorkReferencable {
override val internalPorkElement: PorkElement = element
}

View File

@ -26,6 +26,7 @@
<lang.elementManipulator
implementationClass="gay.pizza.pork.idea.PorkElementManipulator"
forClass="gay.pizza.pork.idea.psi.gen.PorkElement"/>
<psi.declarationProvider implementation="gay.pizza.pork.idea.PorkSymbolDeclarationProvider"/>
</extensions>
<applicationListeners>