Skip to content

Commit 0bd3ff7

Browse files
committed
fix: Fix imports and hints
1 parent a32d4ec commit 0bd3ff7

File tree

6 files changed

+921
-26
lines changed

6 files changed

+921
-26
lines changed

src/main/kotlin/org/rhai/features/RhaiCompletionContributor.kt

Lines changed: 277 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.intellij.util.ProcessingContext
1111
import org.rhai.*
1212
import org.rhai.lang.RhaiFile
1313
import org.rhai.registry.RhaiRegistryProvider
14+
import org.rhai.util.RhaiImportUtils
1415

1516
class RhaiCompletionContributor : CompletionContributor() {
1617

@@ -39,26 +40,62 @@ class RhaiCompletionContributor : CompletionContributor() {
3940
val position = parameters.position
4041
val file = parameters.originalFile as? RhaiFile ?: return
4142

42-
// Add keywords
43-
addKeywords(result)
43+
// Check if we're in a member access context (after dot or ::)
44+
val isAfterDot = isAfterDot(position)
45+
val isAfterDoubleColon = isAfterDoubleColon(position)
4446

45-
// Add builtin functions
46-
addBuiltinFunctions(result)
47+
// Don't add keywords, snippets, or cross-file symbols after dot or ::
48+
if (!isAfterDot && !isAfterDoubleColon) {
49+
// Add keywords
50+
addKeywords(result)
51+
52+
// Add snippets/templates
53+
addSnippets(result)
54+
55+
// Add symbols from other files (with import hint)
56+
addCrossFileSymbols(parameters, file, result)
57+
}
58+
59+
// Add builtin functions (these can be called as methods too)
60+
if (!isAfterDot) {
61+
addBuiltinFunctions(result)
62+
}
4763

4864
// Add custom registered functions/variables from Rust
49-
addCustomRegistryItems(parameters, result)
65+
if (!isAfterDot) {
66+
addCustomRegistryItems(parameters, result)
67+
}
5068

5169
// Add user-defined functions
52-
addUserFunctions(file, result)
70+
if (!isAfterDot) {
71+
addUserFunctions(file, result)
72+
}
5373

54-
// Add variables in scope
55-
addVariablesInScope(position, file, result)
74+
// Add variables in scope (not after dot or ::)
75+
if (!isAfterDot && !isAfterDoubleColon) {
76+
addVariablesInScope(position, file, result)
77+
}
5678

57-
// Add constants
58-
addConstants(file, result)
79+
// Add constants (not after dot)
80+
if (!isAfterDot && !isAfterDoubleColon) {
81+
addConstants(file, result)
82+
}
83+
}
5984

60-
// Add snippets/templates
61-
addSnippets(result)
85+
private fun isAfterDot(position: PsiElement): Boolean {
86+
var sibling = position.prevSibling
87+
while (sibling != null && sibling.node.elementType == com.intellij.psi.TokenType.WHITE_SPACE) {
88+
sibling = sibling.prevSibling
89+
}
90+
return sibling?.node?.elementType == RhaiTypes.DOT
91+
}
92+
93+
private fun isAfterDoubleColon(position: PsiElement): Boolean {
94+
var sibling = position.prevSibling
95+
while (sibling != null && sibling.node.elementType == com.intellij.psi.TokenType.WHITE_SPACE) {
96+
sibling = sibling.prevSibling
97+
}
98+
return sibling?.node?.elementType == RhaiTypes.DOUBLE_COLON
6299
}
63100

64101
private fun addKeywords(result: CompletionResultSet) {
@@ -247,6 +284,91 @@ class RhaiCompletionContributor : CompletionContributor() {
247284
}
248285
}
249286

287+
private fun addCrossFileSymbols(parameters: CompletionParameters, currentFile: RhaiFile, result: CompletionResultSet) {
288+
val virtualFile = currentFile.virtualFile ?: return
289+
val project = parameters.position.project
290+
291+
// Get all Rhai files in project
292+
val rhaiFiles = com.intellij.psi.search.FileTypeIndex.getFiles(
293+
org.rhai.lang.RhaiFileType.INSTANCE,
294+
com.intellij.psi.search.GlobalSearchScope.projectScope(project)
295+
)
296+
297+
val psiManager = com.intellij.psi.PsiManager.getInstance(project)
298+
299+
for (file in rhaiFiles) {
300+
if (file == virtualFile) continue
301+
302+
val psiFile = psiManager.findFile(file) as? RhaiFile ?: continue
303+
val moduleName = file.nameWithoutExtension
304+
305+
// Add all functions from other files (pub first, then private)
306+
PsiTreeUtil.findChildrenOfType(psiFile, RhaiFunctionDefinition::class.java)
307+
.forEach { func ->
308+
val name = func.identifier.text
309+
val params = func.parameters?.parameterList?.joinToString(", ") { it.pattern.text } ?: ""
310+
val isPublic = func.text.trimStart().startsWith("pub ")
311+
val visibility = if (isPublic) "" else " (private)"
312+
313+
result.addElement(
314+
LookupElementBuilder.create("$moduleName::$name")
315+
.withLookupString(name) // Also match by just the function name
316+
.withPresentableText(name)
317+
.withTypeText("from $moduleName$visibility")
318+
.withTailText("($params)", true)
319+
.withIcon(AllIcons.Nodes.Function)
320+
.withInsertHandler(CrossFileSymbolInsertHandler(file, moduleName, name, true))
321+
)
322+
}
323+
324+
// Add all constants from other files
325+
PsiTreeUtil.findChildrenOfType(psiFile, RhaiConstDeclaration::class.java)
326+
.forEach { constDecl ->
327+
constDecl.node.findChildByType(RhaiTypes.IDENTIFIER)?.let { idNode ->
328+
val name = idNode.text
329+
val isPublic = constDecl.text.trimStart().startsWith("pub ")
330+
val visibility = if (isPublic) "" else " (private)"
331+
332+
result.addElement(
333+
LookupElementBuilder.create("$moduleName::$name")
334+
.withLookupString(name)
335+
.withPresentableText(name)
336+
.withTypeText("const from $moduleName$visibility")
337+
.withIcon(AllIcons.Nodes.Constant)
338+
.withInsertHandler(CrossFileSymbolInsertHandler(file, moduleName, name, false))
339+
)
340+
}
341+
}
342+
343+
// Add top-level let declarations (global variables) from other files
344+
PsiTreeUtil.findChildrenOfType(psiFile, RhaiLetDeclaration::class.java)
345+
.filter { isTopLevelDeclaration(it, psiFile) }
346+
.forEach { letDecl ->
347+
val name = letDecl.pattern.text
348+
349+
result.addElement(
350+
LookupElementBuilder.create("$moduleName::$name")
351+
.withLookupString(name)
352+
.withPresentableText(name)
353+
.withTypeText("var from $moduleName")
354+
.withIcon(AllIcons.Nodes.Variable)
355+
.withInsertHandler(CrossFileSymbolInsertHandler(file, moduleName, name, false))
356+
)
357+
}
358+
}
359+
}
360+
361+
private fun isTopLevelDeclaration(letDecl: RhaiLetDeclaration, file: RhaiFile): Boolean {
362+
var parent = letDecl.parent
363+
while (parent != null && parent !is RhaiFile) {
364+
if (parent is RhaiFunctionDefinition || parent is RhaiClosureExpr) {
365+
return false // Inside a function/closure
366+
}
367+
parent = parent.parent
368+
}
369+
return true
370+
}
371+
250372
private fun inferType(expression: RhaiExpression?): String {
251373
if (expression == null) return "Dynamic"
252374

@@ -309,27 +431,79 @@ class RhaiCompletionContributor : CompletionContributor() {
309431
override fun handleInsert(context: InsertionContext, item: LookupElement) {
310432
val editor = context.editor
311433
val document = editor.document
434+
val tailOffset = context.tailOffset
435+
436+
// Check if there's already a space after the insertion point
437+
val hasSpaceAfter = tailOffset < document.textLength &&
438+
document.charsSequence[tailOffset] == ' '
312439

313440
when (keyword) {
314-
"if", "while", "for", "switch" -> {
315-
document.insertString(context.tailOffset, " ")
316-
editor.caretModel.moveToOffset(context.tailOffset + 1)
441+
// Control flow keywords - need space before condition/expression
442+
"if", "while", "for", "switch", "in" -> {
443+
if (!hasSpaceAfter) {
444+
document.insertString(tailOffset, " ")
445+
}
446+
editor.caretModel.moveToOffset(tailOffset + 1)
317447
}
448+
// Function definition - insert template
318449
"fn" -> {
319-
document.insertString(context.tailOffset, " () {\n \n}")
320-
editor.caretModel.moveToOffset(context.tailOffset + 2)
450+
document.insertString(tailOffset, " () {\n \n}")
451+
editor.caretModel.moveToOffset(tailOffset + 2)
321452
}
453+
// Declarations - need space before name
322454
"let", "const" -> {
323-
document.insertString(context.tailOffset, " ")
324-
editor.caretModel.moveToOffset(context.tailOffset + 1)
455+
if (!hasSpaceAfter) {
456+
document.insertString(tailOffset, " ")
457+
}
458+
editor.caretModel.moveToOffset(tailOffset + 1)
325459
}
460+
// Return/throw - need space before value
326461
"return", "throw" -> {
327-
document.insertString(context.tailOffset, " ")
328-
editor.caretModel.moveToOffset(context.tailOffset + 1)
462+
if (!hasSpaceAfter) {
463+
document.insertString(tailOffset, " ")
464+
}
465+
editor.caretModel.moveToOffset(tailOffset + 1)
329466
}
467+
// Import - insert quotes for module path
330468
"import" -> {
331-
document.insertString(context.tailOffset, " \"\"")
332-
editor.caretModel.moveToOffset(context.tailOffset + 2)
469+
document.insertString(tailOffset, " \"\"")
470+
editor.caretModel.moveToOffset(tailOffset + 2)
471+
}
472+
// Visibility modifiers - need space before fn/let/const
473+
"pub", "private" -> {
474+
if (!hasSpaceAfter) {
475+
document.insertString(tailOffset, " ")
476+
}
477+
editor.caretModel.moveToOffset(tailOffset + 1)
478+
}
479+
// Module/export - need space before name
480+
"module", "export" -> {
481+
if (!hasSpaceAfter) {
482+
document.insertString(tailOffset, " ")
483+
}
484+
editor.caretModel.moveToOffset(tailOffset + 1)
485+
}
486+
// Else - can be followed by space (else if) or brace (else {)
487+
"else" -> {
488+
if (!hasSpaceAfter) {
489+
document.insertString(tailOffset, " ")
490+
}
491+
editor.caretModel.moveToOffset(tailOffset + 1)
492+
}
493+
// Try block - insert template
494+
"try" -> {
495+
document.insertString(tailOffset, " {\n \n}")
496+
editor.caretModel.moveToOffset(tailOffset + 6)
497+
}
498+
// Catch - need space before exception variable
499+
"catch" -> {
500+
document.insertString(tailOffset, " (err) {\n \n}")
501+
editor.caretModel.moveToOffset(tailOffset + 2)
502+
}
503+
// Loop - insert template
504+
"loop" -> {
505+
document.insertString(tailOffset, " {\n \n}")
506+
editor.caretModel.moveToOffset(tailOffset + 6)
333507
}
334508
}
335509
}
@@ -350,6 +524,86 @@ class RhaiCompletionContributor : CompletionContributor() {
350524
}
351525
}
352526

527+
/**
528+
* Insert handler for cross-file symbols.
529+
* Adds the import statement if not already present and inserts the qualified reference.
530+
*/
531+
private class CrossFileSymbolInsertHandler(
532+
private val sourceFile: com.intellij.openapi.vfs.VirtualFile,
533+
private val moduleName: String,
534+
private val symbolName: String,
535+
private val isFunction: Boolean
536+
) : InsertHandler<LookupElement> {
537+
override fun handleInsert(context: InsertionContext, item: LookupElement) {
538+
val editor = context.editor
539+
val document = editor.document
540+
val project = context.project
541+
val psiFile = context.file as? RhaiFile ?: return
542+
val currentFile = psiFile.virtualFile ?: return
543+
544+
// Save positions BEFORE any modifications
545+
// At this point, completion has already inserted "moduleName::symbolName"
546+
val completionStart = context.startOffset
547+
val completionEnd = context.tailOffset
548+
549+
// Calculate relative import path
550+
val importPath = RhaiProjectSymbolsProvider.getRelativeImportPath(currentFile, sourceFile)
551+
552+
// Check if import already exists using document text (more reliable than PSI)
553+
val fileText = document.text
554+
val existingAlias = RhaiImportUtils.findImportAliasInText(fileText, importPath)
555+
556+
val actualModuleName: String
557+
var adjustment = 0
558+
559+
if (existingAlias != null) {
560+
// Use existing alias - no import needed
561+
actualModuleName = existingAlias
562+
} else {
563+
// Add new import at the appropriate location
564+
// Use PSI for finding insert position (it's more accurate for structure)
565+
com.intellij.psi.PsiDocumentManager.getInstance(project).commitDocument(document)
566+
val insertOffset = RhaiImportUtils.findImportInsertOffset(psiFile)
567+
val importStatement = RhaiImportUtils.buildImportStatement(importPath, moduleName)
568+
569+
document.insertString(insertOffset, importStatement)
570+
571+
// If import was inserted BEFORE the completion, adjust offsets
572+
if (insertOffset <= completionStart) {
573+
adjustment = importStatement.length
574+
}
575+
576+
actualModuleName = moduleName
577+
}
578+
579+
// The completion inserted "moduleName::symbolName"
580+
// If actualModuleName differs from moduleName, we need to replace the prefix
581+
if (actualModuleName != moduleName) {
582+
val adjustedStart = completionStart + adjustment
583+
val adjustedEnd = completionEnd + adjustment
584+
585+
val oldText = "$moduleName::$symbolName"
586+
val newText = "$actualModuleName::$symbolName"
587+
588+
document.replaceString(adjustedStart, adjustedEnd, newText)
589+
590+
// Add () for functions
591+
if (isFunction) {
592+
val funcTail = adjustedStart + newText.length
593+
document.insertString(funcTail, "()")
594+
editor.caretModel.moveToOffset(funcTail + 1)
595+
}
596+
} else {
597+
// Module name is the same, just add () for functions
598+
if (isFunction) {
599+
val funcTail = completionEnd + adjustment
600+
document.insertString(funcTail, "()")
601+
editor.caretModel.moveToOffset(funcTail + 1)
602+
}
603+
}
604+
}
605+
}
606+
353607
data class FunctionInfo(val params: String, val returnType: String)
354608
data class Snippet(val template: String)
355609

0 commit comments

Comments
 (0)