Phase 5: add rename and find usages

This commit is contained in:
hgranthorner
2026-08-04 15:54:31 -04:00
parent 4758bf12e2
commit 460fc969db
17 changed files with 293 additions and 25 deletions

View File

@@ -8,12 +8,12 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.util.PsiTreeUtil;
import static dev.hgh.jai.psi.JaiTypes.*;
import com.intellij.extapi.psi.ASTWrapperPsiElement;
import dev.hgh.jai.psi.mixin.JaiNamedElementMixin;
import dev.hgh.jai.psi.*;
public class JaiDeclNameImpl extends ASTWrapperPsiElement implements JaiDeclName {
public class JaiDeclNameImpl extends JaiNamedElementMixin implements JaiDeclName {
public JaiDeclNameImpl(@NotNull ASTNode node) {
public JaiDeclNameImpl(ASTNode node) {
super(node);
}

View File

@@ -227,6 +227,7 @@ private declModifier_ ::= <<dirWithBlock>> directiveFlag_* block
private usingModifier_ ::= 'using' [directiveFlag_ [<<exprNoAssign>>]]
declNames ::= declName (['='] ',' declName ['='])* ['=']
declName ::= ['`'] (IDENT | keywordAsName_)
{ mixin="dev.hgh.jai.psi.mixin.JaiNamedElementMixin" }
private declTail_ ::= ':' [<<exprNoAssign>>] procModifier* [',' assignTargets_] [('=' | ':') initializerList_]
| ':=' initializerList_
| '::' initializerList_

View File

@@ -0,0 +1,41 @@
package dev.hgh.jai.findusages
import com.intellij.lang.cacheBuilder.DefaultWordsScanner
import com.intellij.lang.cacheBuilder.WordsScanner
import com.intellij.lang.findUsages.FindUsagesProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.PsiNamedElement
import com.intellij.psi.tree.TokenSet
import dev.hgh.jai.lexer.JaiLexer
import dev.hgh.jai.lexer.JaiTokenTypes
import dev.hgh.jai.psi.JaiRefExpr
/** Supplies word indexing and descriptions for Jai declaration usages. */
class JaiFindUsagesProvider : FindUsagesProvider {
override fun getWordsScanner(): WordsScanner =
DefaultWordsScanner(
JaiLexer(),
TokenSet.create(JaiTokenTypes.IDENT),
JaiTokenTypes.COMMENTS,
JaiTokenTypes.STRINGS,
)
override fun canFindUsagesFor(psiElement: PsiElement): Boolean = psiElement is PsiNameIdentifierOwner || psiElement is JaiRefExpr
override fun getHelpId(psiElement: PsiElement): String? = null
override fun getType(element: PsiElement): String =
if (element is PsiNameIdentifierOwner || element is JaiRefExpr) {
"Jai declaration"
} else {
""
}
override fun getDescriptiveName(element: PsiElement): String = (element as? PsiNamedElement)?.name ?: element.text
override fun getNodeText(
element: PsiElement,
useFullName: Boolean,
): String = element.text
}

View File

@@ -0,0 +1,23 @@
package dev.hgh.jai.psi
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.util.PsiTreeUtil
import dev.hgh.jai.JaiLanguage
import dev.hgh.jai.lexer.JaiTokenTypes
/** Creates small Jai PSI fragments for AST replacements used by rename refactoring. */
object JaiPsiElementFactory {
fun createIdentifier(
project: Project,
name: String,
): PsiElement {
val file = PsiFileFactory.getInstance(project).createFileFromText("rename.jai", JaiLanguage, "$name :: 0;")
val declarationName =
PsiTreeUtil.findChildOfType(file, JaiDeclName::class.java)
?: error("Could not parse a Jai declaration name for '$name'")
return declarationName.node.findChildByType(JaiTokenTypes.IDENT)?.psi
?: error("Jai name '$name' is not an identifier")
}
}

View File

@@ -0,0 +1,28 @@
package dev.hgh.jai.psi.mixin
import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiNameIdentifierOwner
import dev.hgh.jai.lexer.JaiTokenTypes
import dev.hgh.jai.psi.JaiPsiElementFactory
/** Gives declaration names the standard PSI naming contract used by rename/find-usages. */
abstract class JaiNamedElementMixin(
node: ASTNode,
) : ASTWrapperPsiElement(node),
PsiNameIdentifierOwner {
override fun getNameIdentifier(): PsiElement? = node.findChildByType(JaiTokenTypes.IDENT)?.psi
override fun getName(): String? = nameIdentifier?.text
override fun setName(name: String): PsiElement {
val identifier = nameIdentifier ?: return this
identifier.replace(JaiPsiElementFactory.createIdentifier(project, name))
return this
}
override fun getNavigationElement(): PsiElement = nameIdentifier ?: this
override fun getTextOffset(): Int = nameIdentifier?.textOffset ?: super.getTextOffset()
}

View File

@@ -0,0 +1,26 @@
package dev.hgh.jai.refactoring
import com.intellij.lang.refactoring.NamesValidator
import com.intellij.openapi.project.Project
import dev.hgh.jai.lexer.JaiTokenTypes
/** Applies Jai's ASCII identifier and keyword rules to rename input. */
class JaiNamesValidator : NamesValidator {
override fun isKeyword(
name: String,
project: Project?,
): Boolean = name in JaiTokenTypes.KEYWORD_MAP
override fun isIdentifier(
name: String,
project: Project?,
): Boolean {
if (name.isEmpty() || isKeyword(name, project)) return false
if (!isIdentifierStart(name.first())) return false
return name.drop(1).all(::isIdentifierPart)
}
private fun isIdentifierStart(character: Char): Boolean = character in 'a'..'z' || character in 'A'..'Z' || character == '_'
private fun isIdentifierPart(character: Char): Boolean = isIdentifierStart(character) || character in '0'..'9'
}

View File

@@ -0,0 +1,14 @@
package dev.hgh.jai.refactoring
import com.intellij.lang.refactoring.RefactoringSupportProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiNameIdentifierOwner
import dev.hgh.jai.JaiLanguage
/** Enables standard and in-place rename for Jai declaration names. */
class JaiRefactoringSupportProvider : RefactoringSupportProvider() {
override fun isMemberInplaceRenameAvailable(
elementToRename: PsiElement,
context: PsiElement?,
): Boolean = elementToRename is PsiNameIdentifierOwner && elementToRename.language == JaiLanguage
}

View File

@@ -22,6 +22,7 @@ import dev.hgh.jai.psi.JaiDeclName
import dev.hgh.jai.psi.JaiDeclaration
import dev.hgh.jai.psi.JaiDirectiveExpr
import dev.hgh.jai.psi.JaiLiteralExpr
import dev.hgh.jai.psi.JaiPsiElementFactory
import dev.hgh.jai.psi.JaiRefExpr
/** Adds file references for directives and symbol references for Jai identifiers. */
@@ -106,6 +107,12 @@ private class JaiSymbolReference(
) : PsiReferenceBase<JaiRefExpr>(sourceElement, TextRange(0, sourceElement.textLength), true) {
override fun resolve(): PsiElement? = JaiSymbolResolver.resolve(sourceElement)
override fun handleElementRename(newElementName: String): PsiElement {
val identifier = sourceElement.node.findChildByType(JaiTokenTypes.IDENT)?.psi ?: return sourceElement
identifier.replace(JaiPsiElementFactory.createIdentifier(sourceElement.project, newElementName))
return sourceElement
}
override fun getVariants(): Array<Any> = emptyArray()
}

View File

@@ -41,6 +41,15 @@
<completion.contributor
language="Jai"
implementationClass="dev.hgh.jai.completion.JaiCompletionContributor"/>
<lang.refactoringSupport
language="Jai"
implementationClass="dev.hgh.jai.refactoring.JaiRefactoringSupportProvider"/>
<lang.namesValidator
language="Jai"
implementationClass="dev.hgh.jai.refactoring.JaiNamesValidator"/>
<lang.findUsagesProvider
language="Jai"
implementationClass="dev.hgh.jai.findusages.JaiFindUsagesProvider"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,37 @@
package dev.hgh.jai.findusages
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import dev.hgh.jai.psi.JaiDeclName
class JaiFindUsagesTest : BasePlatformTestCase() {
fun testFindUsagesActionFindsProcedureReference() {
val usages = myFixture.testFindUsages("Declaration.jai", "Usage.jai")
assertEquals(1, usages.size)
}
fun testFindsProcedureReferencesFromDeclaration() {
val source =
myFixture.addFileToProject(
"find-usages.jai",
"""
helper :: () {}
first :: () { helper(); }
second :: () { helper(); }
""".trimIndent(),
)
val declaration =
PsiTreeUtil
.findChildrenOfType(source, JaiDeclName::class.java)
.single { it.text == "helper" }
val usages = ReferencesSearch.search(declaration).findAll()
assertEquals(2, usages.size)
assertTrue(usages.all { it.element.text == "helper" })
}
override fun getTestDataPath(): String = "src/test/testData/findusages"
}

View File

@@ -0,0 +1,59 @@
package dev.hgh.jai.refactoring
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.testFramework.fixtures.BasePlatformTestCase
class JaiRenameTest : BasePlatformTestCase() {
fun testDeclarationNamesParticipateInRename() {
myFixture.configureByText(
"rename.jai",
"""
helper<caret> :: () {}
main :: () { helper(); }
""".trimIndent(),
)
val declaration = myFixture.elementAtCaret
assertTrue("declaration name should be a named PSI element", declaration is PsiNameIdentifierOwner)
assertEquals("helper", (declaration as PsiNameIdentifierOwner).name)
}
fun testRenameProcedureUpdatesSameFileReferences() {
myFixture.configureByText(
"rename.jai",
"""
helper<caret> :: () {}
main :: () { helper(); }
""".trimIndent(),
)
myFixture.renameElementAtCaret("renamed")
myFixture.checkResult(
"""
renamed :: () {}
main :: () { renamed(); }
""".trimIndent(),
)
}
fun testRenameTypeUpdatesTypeReferences() {
myFixture.configureByText(
"rename-type.jai",
"""
Point<caret> :: struct { x: int; }
use_point :: (point: Point) -> int { return point.x; }
""".trimIndent(),
)
myFixture.renameElementAtCaret("Coordinate")
myFixture.checkResult(
"""
Coordinate :: struct { x: int; }
use_point :: (point: Coordinate) -> int { return point.x; }
""".trimIndent(),
)
}
}

View File

@@ -0,0 +1 @@
helper<caret> :: () {}

View File

@@ -0,0 +1,2 @@
#load "Declaration.jai";
main :: () { helper(); }