@@ -11,6 +11,7 @@ import com.intellij.util.ProcessingContext
1111import org.rhai.*
1212import org.rhai.lang.RhaiFile
1313import org.rhai.registry.RhaiRegistryProvider
14+ import org.rhai.util.RhaiImportUtils
1415
1516class 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