Behind the Scenes: Reference Completion in the AsyncAPI Plugin for JetBrains IDEs
How to test own implementation of reference contributor in IntelliJ SDK
Today I want to start sharing more about how my AsyncAPI Specification plugin for JetBrains IDEs works under the hood. The goal is to pass along some of the knowledge I’ve gathered and hopefully inspire or help others in the community build awesome plugins
Topic of the Day: Reference Contributors and Auto-Completion
In this post, we’ll explore how the plugin handles custom reference contributors and provides auto-completion for references in JSON-based AsyncAPI documents. I’ll also show what this looks like in practice and how you can test it effectively
Implementation
Rather than dumping large code blocks into this article, I’ll link to relevant source files for further study
These classes relate specifically to JSON:
- AsyncAPISpecificationReferenceContributor: This class is registered as a PSI Reference Contributor for files with the JSON language. It tells the IDE how references should be handled and which strategies are available for resolving them
Providers for different kinds of references:
- AsyncAPIFileReferenceProvider: Handles references to external files
- AsyncAPILocalReferenceProvider: Handles references within the same file
Each provider returns a specific reference type:
- AsyncAPIFileReference: Used for file references
- AsyncAPILocalReference: Used for local references
Finally, the JsonFileVariantsProvider resolves and inspects references, determines if a file should be parsed, and prepares completion suggestions based on the caret position inside a $ref node
Testing
To ensure everything works as expected, we write a test that extends JetBrains’ BasePlatformTestCase
1
2
3
class JsonReferenceVariantsTest: BasePlatformTestCase() {
}
You’ll need to specify the path to the test data:
1
2
3
4
5
class JsonReferenceVariantsTest: BasePlatformTestCase() {
override fun getTestDataPath(): String = "src/test/testData/json/reference/completion/3.0.0"
}
Here’s what you’ll find in that path:
A sample AsyncAPI spec with a $ref
containing a caret position:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"asyncapi": "3.0.0",
"info": {
"title": "Account Service",
"version": "1.0.0",
"description": "This service is in charge of processing user signups :rocket:"
},
"channels": {
"userSignedup": {
"address": "user/signedup",
"messages": {
"userSignedupMessage": {
"$ref": "./ref.json#/r<caret>"
}
}
}
}
}
The file being referenced, containing a simple object:
1
2
3
{
"reference": "reference value"
}
Expected Behavior
When the user types #/r
, the plugin should parse ref.json
and suggest any top-level keys that start with r
. In this case, the expected completion is:
1
#/reference
The Test
Here’s the actual test code:
1
2
3
4
5
fun `test referenced file`() {
val referenceAtCaret = myFixture.getReferenceAtCaretPositionWithAssertion("reference.json", "ref.json")
val lookupElement = referenceAtCaret.variants.first() as LookupElement
assertEquals("./ref.json#/reference", lookupElement.lookupString)
}
Reference - Simple Language Plugin example
Final Thoughts
Custom reference handling is a core feature of a developer-friendly IDE experience. Building intelligent auto-completion and reference resolution helps developers navigate and write AsyncAPI specifications more efficiently
This is just the beginning of the series. Stay tuned for deeper dives into the internals of the plugin — and feel free to explore the AsyncAPI Plugin on GitHub if you’re curious or want to contribute!