How to register a psi file created in memory into a project module?

I want to do something similar to lombok but more.

What lombok does is to add elements to an existing instance of a PsiClass, thus prompting for elements that would otherwise need to be compiled without having to compile them.

I’d like to take it a step further and generate a PsiClass directly in memory to hint at a class that doesn’t actually exist yet.

I already know how to create a PsiFile, now I want to know how to let the idea know that the class “GenerateTest” exists, so that it can be hinted at by “GT”.

// language=JAVA
val java = """
        package top.fallingangel;

        public class GenerateTest {
            public static void test(String[] args) { }
        }
        """.trimIndent()
val javaFile = PsiFileFactory.getInstance(project).createFileFromText("GenerateTest.java", JavaLanguage.INSTANCE, java)

I don’t want to actually generate the physical file of the PsiFile, I just want it to exist in memory.

I tried adding this javaFile to the module like this, but it didn’t work.

val module = ModuleManager.getInstance(project).modules[0]
val modifiableModel = ModuleRootManager.getInstance(module).modifiableModel
val contentEntry = modifiableModel.addContentEntry(javaFile.virtualFile)
contentEntry.addSourceFolder(javaFile.virtualFile, false)
WriteCommandAction.runWriteCommandAction(project) {
    modifiableModel.commit()
}

I followed the approach from CompletionContributor in my plugin to look into the source code of Java and Kotlin plugins.

For Java, class names are obtained from PsiShortNamesCache:

Kotlin also uses PsiShortNamesCache when dealing with Java classes:

When handling Kotlin files, Kotlin uses an implementation of FileBasedIndexExtension, but I can’t delve any deeper… I can’t find the location where this class is used, nor any other related places.

So the question becomes… how can I get the PsiFile created in memory, or rather its corresponding VirtualFile, to be indexed by PsiShortNamesCache and NameByPackageShortNameIndex?

But I think it’s not feasible, because these indices deal with actual existing files, while a VirtualFile in memory is an instance of LightVirtualFile?

So, is my idea feasible? Can IntelliJ IDEA index a VirtualFile in memory?

You cannot add non-physical files to a project like that, so that all subsystems will pick it up.
Could you please explain your use case, what features do you want to add specifically? Does this test class actually exist physically, at a later point?

Let me clarify my specific use case: I’m working with Jimmer’s DTO language - a framework-specific DSL that defines type structures through specialized syntax (not regular Java/Kotlin code). These .dto files get compiled into real Java/Kotlin classes only during build time, which creates an IDE blind spot: until compilation, developers can’t get any code assistance for these not-yet-existing types.

What I’m trying to achieve is creating transient in-memory PsiClasses that mirror what Jimmer’s compiler would produce. These virtual classes would:

  • Provide full code intelligence (completion/navigation)
  • Exist purely in memory without file system traces
  • Automatically retire when real classes are generated

The key difference from Lombok: Instead of augmenting existing classes, I need to materialize complete types from Jimmer’s DSL semantics. When a developer types GT, the IDE should resolve it to the virtual GenerateTest class synthesized from DTO definitions, even though no physical GenerateTest.java exists yet.

My fundamental goal is type hinting for non-existent classes, while creating virtual PsiClass/KtClass instances is just my hypothesized implementation path. After struggling with the PSI integration, I’m starting to question whether this “ghost class” approach is viable at all.

Perhaps there’s a better way to achieve DTO-driven code assistance without full class simulation? For example:

  • Building metadata-backed indexes for DTO types (inspired by index API exploration, feasibility uncertain)
  • Creating light references instead of full PSI hierarchies (does this still involve references to non-existent classes?)
  • Leveraging synthetic code insight providers (but how to handle unresolved reference errors when the hinted classes don’t physically exist?)

Could you suggest alternative strategies to bridge this gap between DSL definitions and IDE awareness?

While working on the Annotator, a new idea emerged:

  1. Code Completion

    • Implement CompletionContributor for Java/Kotlin
    • Synthesize transient PsiClass definitions in memory
    • Feed to CompletionResultSet
  2. Error Suppression

    • Develop custom Annotator specifically for Java/Kotlin
    • Filter false-positive Unresolved reference errors

Would this completion-injection + error-suppression combination work reliably?

Consider providing an implementing of extension point of com.intellij.jvm.elementProvider (com.intellij.lang.jvm.facade.JvmElementProvider interface). This is the entry point for all synthetic declarations visible to Java files.
In this case you won’t need to register synthetic files in the project model, and it will be enough to generate instances of PsiClass on the fly (or cache them in a storage of your choice).

Can IntelliJ IDEA index a VirtualFile in memory?

That’s possible, you can implement a custom virtual file system, and files stored in such a VFS will be indexed. But this is a rather complex and not well documented approach. See com.intellij.openapi.vfs.newvfs.NewVirtualFileSystem as a starting point.

Ok, I’ll try it.

But I see that the collection type returned by JvmElementProvider#getClasses is JvmClass, which is the parent of PsiClass, but not of KtClass… So, does it support kotlin? I’d like to do the same thing for kotlin

@maxmedvedev I’ve tried it, the extension doesn’t work, the returned PsiClass instance doesn’t appear in the hint list, com.intellij.psi.JavaPsiFacade#findClass doesn’t find the PsiClass, and writing out the fully qualified class name doesn’t resolve the PsiClass instance.

class PsiProvider(private val project: Project) : JvmElementProvider {
    @Suppress("UnstableApiUsage")
    override fun getClasses(qualifiedName: String, scope: GlobalSearchScope): MutableList<out JvmClass> {
        val classes = project.service<GeneratedClassCache>()
                .classes
                .toList()
                .filter { it.first.startsWith(qualifiedName) }
                .mapTo(mutableListOf()) { it.second }
        println("getClasses in PsiProvider for `$qualifiedName`: $classes")
        return classes
    }
}

Here’s some of the rest of my code :arrow_down:

@Service(Service.Level.PROJECT)
class GeneratedClassCache(private val project: Project) {
    private val classesCachedValue = CachedValuesManager.getManager(project).createCachedValue {
        @Language("Java")
        val java = """
                    package top.fallingangel;
                    
                    public class GenerateTest {
                        public static void staticMethod1() {}
                    
                        public static void staticMethod2() {}
                    }
                """.trimIndent()
        val javaClass = PsiFileFactory.getInstance(project)
                .createFileFromText("GenerateTest.java", JavaLanguage.INSTANCE, java)
                .getChildOfType<PsiClass>()!!

        CachedValueProvider.Result.create(
            mapOf("top.fallingangel.GenerateTest" to javaClass),
            DumbService.getInstance(project).modificationTracker,
            ProjectRootModificationTracker.getInstance(project),
        )
    }

    val classes = classesCachedValue.value ?: emptyMap()
}

class DTOShortNamesCache(private val project: Project) : PsiShortNamesCache() {
    override fun getClassesByName(name: String, scope: GlobalSearchScope): Array<PsiClass> {
        val classes = project.service<GeneratedClassCache>()
                .classes
                .toList()
                .filter { name in (it.second.name ?: return@filter false) }
                .map { it.second }
                .toTypedArray()
        return classes
    }

    override fun getAllClassNames(): Array<String> {
        val names = project.service<GeneratedClassCache>()
                .classes
                .values
                .mapNotNull { it.name }
                .toTypedArray()
        return names
    }

    // Omit the rest of the methods
}

But I found com.intellij.psi.PsiElementFinder#PsiElementFinder in the comments of JvmElementProvider, and this works… Combined with the PsiShortNamesCache(DTOShortNamesCache) that I found earlier, the code’s completion hints show the PsiClass that I generated, which hints at the class members, and clicking on the jumps also works!

class PsiFinder(private val project: Project) : PsiElementFinder() {
    override fun findClass(qualifiedName: String, scope: GlobalSearchScope): PsiClass? {
        return project.service<GeneratedClassCache>().classes[qualifiedName]
    }

    override fun findClasses(qualifiedName: String, scope: GlobalSearchScope): Array<PsiClass> {
        return project.service<GeneratedClassCache>()
                .classes
                .toList()
                .filter { it.first.startsWith(qualifiedName) }
                .map { it.second }
                .toTypedArray()
    }
}

However, there is another problem, as shown in the screenshot, after the completion prompt, the class name is fully qualified, not import + class name. After using ‘Replace qualified name with import’, it will only remove the package name, but will not generate the import statement, and then it will report an error, and after fixing it, it will become fully qualified again.

If you manually write out the import statement, it resolves to the PsiClass instance, but it prompts that the import statement is not being used and can be optimized away… What is this due to? Need to implement another extension point related to import? Is it com.intellij.lang.importOptimizer? But when I look at the implementation of JavaImportOptimizer, there is parsing of import statements in it, and when I test it under the kotlin file, the import and the parsing of import statements are fine, so… I don’t think the importOptimizer is missing, is it?

Also one more question, under Java this effect is done through com.intellij.java.shortNamesCache and com.intellij.java.elementFinder, which extension point should be for Kotlin?