Skip to content

Commit 2cdb058

Browse files
committed
feat: Add refactoring
1 parent 9c43575 commit 2cdb058

File tree

6 files changed

+490
-1
lines changed

6 files changed

+490
-1
lines changed

.claude/settings.local.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
"Bash(xargs cat:*)",
1515
"Bash(JAVA_HOME=/Users/glebmikhailov/Library/Java/JavaVirtualMachines/corretto-17.0.10/Contents/Home ./gradlew clean build:*)",
1616
"Bash(JAVA_HOME=/Users/glebmikhailov/Library/Java/JavaVirtualMachines/corretto-17.0.10/Contents/Home ./gradlew clean compileKotlin:*)",
17-
"Bash(git stash:*)"
17+
"Bash(git stash:*)",
18+
"Bash(/usr/libexec/java_home:*)",
19+
"Bash(JAVA_HOME=/Users/glebmikhailov/Library/Java/JavaVirtualMachines/corretto-17.0.10/Contents/Home ./gradlew compileKotlin:*)",
20+
"Bash(JAVA_HOME=/Users/glebmikhailov/Library/Java/JavaVirtualMachines/corretto-17.0.10/Contents/Home ./gradlew buildPlugin:*)"
1821
]
1922
}
2023
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.rhai.refactoring
2+
3+
import com.intellij.lang.refactoring.NamesValidator
4+
import com.intellij.openapi.project.Project
5+
6+
/**
7+
* Validates identifier names in Rhai.
8+
* Used by rename refactoring to check if new names are valid.
9+
*/
10+
class RhaiNamesValidator : NamesValidator {
11+
12+
private val keywords = setOf(
13+
"let", "const", "fn", "if", "else", "while", "for", "in",
14+
"loop", "break", "continue", "return", "throw", "try", "catch",
15+
"switch", "default", "import", "export", "module", "private", "pub",
16+
"true", "false", "null", "this", "global", "is", "as", "do", "until",
17+
"shared", "sync", "async", "await", "inf", "NaN", "undefined"
18+
)
19+
20+
override fun isKeyword(name: String, project: Project?): Boolean {
21+
return keywords.contains(name)
22+
}
23+
24+
override fun isIdentifier(name: String, project: Project?): Boolean {
25+
if (name.isEmpty()) return false
26+
27+
// Rhai identifiers: start with letter or underscore, followed by letters, digits, or underscores
28+
val firstChar = name[0]
29+
if (!firstChar.isLetter() && firstChar != '_') return false
30+
31+
return name.all { it.isLetterOrDigit() || it == '_' }
32+
}
33+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.rhai.refactoring
2+
3+
import com.intellij.openapi.project.Project
4+
import com.intellij.psi.PsiElement
5+
import com.intellij.psi.PsiFileFactory
6+
import com.intellij.psi.util.PsiTreeUtil
7+
import org.rhai.RhaiLetDeclaration
8+
import org.rhai.RhaiTypes
9+
import org.rhai.lang.RhaiFile
10+
import org.rhai.lang.RhaiFileType
11+
12+
/**
13+
* Factory for creating Rhai PSI elements.
14+
* Used by refactoring operations to create new elements.
15+
*/
16+
class RhaiPsiFactory(private val project: Project) {
17+
18+
/**
19+
* Create an identifier element with the given name.
20+
*/
21+
fun createIdentifier(name: String): PsiElement {
22+
// Create a simple let declaration to extract the identifier
23+
val file = createFile("let $name = 0;")
24+
val letDecl = PsiTreeUtil.findChildOfType(file, RhaiLetDeclaration::class.java)
25+
?: throw IllegalStateException("Failed to create identifier PSI element")
26+
27+
val identifierNode = letDecl.pattern.node.findChildByType(RhaiTypes.IDENTIFIER)
28+
?: throw IllegalStateException("Failed to find identifier in created element")
29+
30+
return identifierNode.psi
31+
}
32+
33+
/**
34+
* Create a Rhai file with the given content.
35+
*/
36+
fun createFile(content: String): RhaiFile {
37+
return PsiFileFactory.getInstance(project)
38+
.createFileFromText("dummy.rhai", RhaiFileType.INSTANCE, content) as RhaiFile
39+
}
40+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package org.rhai.refactoring
2+
3+
import com.intellij.openapi.actionSystem.CommonDataKeys
4+
import com.intellij.openapi.actionSystem.DataContext
5+
import com.intellij.openapi.editor.Editor
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.psi.PsiElement
8+
import com.intellij.psi.PsiFile
9+
import com.intellij.psi.util.elementType
10+
import com.intellij.refactoring.rename.PsiElementRenameHandler
11+
import com.intellij.refactoring.rename.RenameHandler
12+
import org.rhai.RhaiTypes
13+
import org.rhai.lang.RhaiFile
14+
15+
/**
16+
* Rename handler for Rhai identifiers.
17+
* Enables Shift+F6 rename refactoring for variables, functions, constants, etc.
18+
*/
19+
class RhaiRenameHandler : RenameHandler {
20+
21+
override fun isAvailableOnDataContext(dataContext: DataContext): Boolean {
22+
val element = getTargetElement(dataContext) ?: return false
23+
return isRenamableElement(element)
24+
}
25+
26+
override fun isRenaming(dataContext: DataContext): Boolean {
27+
return isAvailableOnDataContext(dataContext)
28+
}
29+
30+
override fun invoke(project: Project, editor: Editor?, file: PsiFile?, dataContext: DataContext) {
31+
val element = getTargetElement(dataContext) ?: return
32+
if (editor == null || file == null) return
33+
34+
if (isRenamableElement(element)) {
35+
// Use built-in rename dialog with our element
36+
PsiElementRenameHandler.invoke(element, project, element, editor)
37+
}
38+
}
39+
40+
override fun invoke(project: Project, elements: Array<out PsiElement>, dataContext: DataContext) {
41+
if (elements.isNotEmpty()) {
42+
val editor = CommonDataKeys.EDITOR.getData(dataContext)
43+
if (editor != null) {
44+
PsiElementRenameHandler.invoke(elements[0], project, elements[0], editor)
45+
}
46+
}
47+
}
48+
49+
private fun getTargetElement(dataContext: DataContext): PsiElement? {
50+
val editor = CommonDataKeys.EDITOR.getData(dataContext) ?: return null
51+
val file = CommonDataKeys.PSI_FILE.getData(dataContext) ?: return null
52+
53+
if (file !is RhaiFile) return null
54+
55+
val offset = editor.caretModel.offset
56+
return file.findElementAt(offset)
57+
}
58+
59+
private fun isRenamableElement(element: PsiElement): Boolean {
60+
// Only allow renaming IDENTIFIER tokens
61+
if (element.elementType != RhaiTypes.IDENTIFIER) return false
62+
63+
val name = element.text
64+
65+
// Don't allow renaming keywords
66+
val keywords = setOf(
67+
"let", "const", "fn", "if", "else", "while", "for", "in",
68+
"loop", "break", "continue", "return", "throw", "try", "catch",
69+
"switch", "default", "import", "export", "module", "private", "pub",
70+
"true", "false", "null", "this", "global", "is", "as"
71+
)
72+
if (keywords.contains(name)) return false
73+
74+
// Don't allow renaming built-in functions
75+
val builtins = setOf(
76+
"print", "println", "debug", "type_of", "is_def",
77+
"to_string", "to_int", "to_float", "to_decimal",
78+
"abs", "sin", "cos", "tan", "sqrt", "exp", "ln", "log",
79+
"push", "pop", "shift", "insert", "remove", "reverse", "sort",
80+
"map", "filter", "reduce", "some", "all", "for_each",
81+
"keys", "values", "len", "contains", "trim", "replace", "split"
82+
)
83+
if (builtins.contains(name)) return false
84+
85+
return true
86+
}
87+
}

0 commit comments

Comments
 (0)