It runs on my machine 01
October 5, 2025
Introduction
I've been using LLMs for code assistance, in the form of completion/copilot suggestions or through a CLI like Claude Code, for some time now, and while I've got a running list of things I like and do not like, I can see value in the idea of LLM-assisted code writing.
Some things keep nagging at me:
- the tools do not work on a plane i.e., when there is no internet connection or when I'm not otherwise connected
- I've got a powerful laptop: why do I have to use it like a terminal in the original sense of the word? The compute power is not local to my machine, it's in a server somewhere I have to connect to, and all my RAM and CPU/GPU is wasted
- I have only a superficial understanding of how these tools work in the context of the IDE I use for work all-day long
The last point, in particular, triggers me into wanting to build my own copilot plugin for my IDE of choice: an IntelliJ one and, more specifically, PHPStorm.
Previous attempts and art
I've tried to switch away from PHPStorm to other Visual-Studio-Code-like forks and variations, but the muscle memory,
the tooling and the code inspection capabilities are not where I would like them to be.
I've tried setting up the VS-Code-likes using all the recommended plugins and tools (including licensed ones), I've
tried other IDEs as well (e.g. Zed or neovim) and all of them are great.
But they are not great for me, my idea of IDE perfection that I get out of the box with PHPStorm always another
plugin/extension away.
The one thing that really surprised me is the llama.vim plugins for vim/neovim.
With a relatively small model (Qwen2.5-Coder-3B
) running on my laptop, it provides excellent suggestion with a "
look-around" quality that other tools lack.
The lack of an IntelliJ version is the inspiration for this project of mine.
Can I build an IntelliJ plugin providing auto-completions inspired by the llama.vim
plugin?
It should be noted I know nothing of Kotlin, the language I will use to build the plugin.
Level 0
I've done some research, and if I'm building a plugin for an IntelliJ-based IDE, I should use
the plugin template.
Once cloned and configured I can run build the plugin using the gradlew buildPlugin
task and install it in my IDE of
choice (PHPStorm) from the Plugins
section of the IDE using the Install Plugin from Disk
option.
I've customized the project and set its name: "completamente".
It means "completely" in Italian and, again in Italian is a composition of the words "complete" and "mind".
It's meant to be a word play around the concept of completion and mind.
Naming things is hard; I'm not a poet.
Since building the plugin and installing it in my work IDE (i.e. the IDE I work with all-day) is not ideal, I've set up a run configuration to run the plugin in PHPStorm to debug it.
The CLI command to start a different copy of PHPStorm with my plugin installed in it would be:
./gradlew runIdeForUiTests -PplatformType=PS -PplatformVersion=2025.1.3
This is convenient, but I'm still not getting a step debugging environment with this.
To start a debug version of PHPStorm including my plugin I've added a run configuration to the project in the
.run/Run Plugin in PHPStorm.run.xml
file:
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Plugin in PHPStorm" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="IDE logs" path="$PROJECT_DIR$/build/idea-sandbox/*/log/idea.log" show_all="true"/>
<ExternalSystemSettings>
<option name="executionName"/>
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
<option name="externalSystemIdString" value="GRADLE"/>
<option name="scriptParameters" value=""/>
<option name="taskDescriptions">
<list/>
</option>
<option name="taskNames">
<list>
<option value="runIde"/>
</list>
</option>
<option name="vmOptions" value=""/>
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2"/>
</configuration>
</component>
Now I can run or debug a copy of PHPStorm with my plugin installed:
Tapping into the completion API
The first task is connecting my plugin to the reccomended IDE completion API through the plugin configuration file (
src/main/resources/META-INF/plugin.xml
):
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html -->
<idea-plugin>
<id>com.github.lucatume.completamente</id>
<name>completamente</name>
<vendor>lucatume</vendor>
<depends>com.intellij.modules.platform</depends>
<resource-bundle>messages.MyBundle</resource-bundle>
<extensions defaultExtensionNs="com.intellij">
<inline.completion.provider implementation="com.github.lucatume.completamente.completion.Service"/>
</extensions>
</idea-plugin>
The Service
class is, at this stage, not doing much if not providing the same completion, "Hello World!" over and
over:
package com.github.lucatume.completamente.completion
import com.intellij.codeInsight.inline.completion.InlineCompletionEvent
import com.intellij.codeInsight.inline.completion.InlineCompletionProvider
import com.intellij.codeInsight.inline.completion.InlineCompletionProviderID
import com.intellij.codeInsight.inline.completion.InlineCompletionRequest
import com.intellij.codeInsight.inline.completion.elements.InlineCompletionGrayTextElement
import com.intellij.codeInsight.inline.completion.suggestion.InlineCompletionSingleSuggestion
import com.intellij.codeInsight.inline.completion.suggestion.InlineCompletionSuggestion
import com.intellij.codeInsight.inline.completion.suggestion.InlineCompletionVariant
class Service : InlineCompletionProvider {
// A class to represent a single string suggestion.
class StringSuggestion(private val suggestion: String) : InlineCompletionSingleSuggestion {
override suspend fun getVariant() = InlineCompletionVariant.build {
emit(InlineCompletionGrayTextElement(suggestion))
}
}
override val id: InlineCompletionProviderID
get() = InlineCompletionProviderID("completamente")
override suspend fun getSuggestion(request: InlineCompletionRequest): InlineCompletionSuggestion {
// No computation going on here, the same string every time.
return StringSuggestion("Hello World!")
}
override fun isEnabled(event: InlineCompletionEvent): Boolean {
// It's always enabled.
return true
}
}
I've started the plugin in debug mode and I can see the completion in action in the context of my di52
project:
It works. Badly and poorly, but it compiles and it works.
Tests
I'm doing this project to learn.
The easiest way for me to learn things is to test them.
I've started simple with a test to cover the really basic StringSuggestion
class:
// src/test/kotlin/com/github/lucatume/completamente/completion/StringSuggestionTest.kt
package com.github.lucatume.completamente.completion
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import kotlinx.coroutines.flow.count
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
class StringSuggestionTest : BasePlatformTestCase() {
fun testItReturnsTheSameString() {
val stringSuggestion = StringSuggestion("hello")
val variant = runBlocking { stringSuggestion.getVariant() }
val size = runBlocking { variant.elements.count() }
val suggestedString = runBlocking { variant.elements.first().text }
assertEquals(1, size)
assertEquals("hello", suggestedString)
}
}
Things got complicated as I entered the world of Kotlin and found myself immediately staring down the abyss of async, multiprocess code.
The StringSuggestion::getVariant
function is marked with the suspend
keyword, which means it's going to be executed
in a different thread; asynchronously, that is.
From my meager understanding of Kotlin coroutines, I've worked out that suspend
is like JavaScript async
and runBlocking
is like await
.
If I was to write the code in JavaScript, I'd write something like this:
const variant = await stringSuggestion.getVariant();
// The `count` function is a coroutine that returns the number of elements in the flow.
// That too is asynchronous.
const size = await variant.elements.count();
// The `first` function is a coroutine that returns the first element in the flow.
// That too is ... asynchronous.
const suggestedString = await variant.elements.first().text;
Running the tests with gradlew test
shows the test passing.
Testing the service itself required me to dig a bit into the IntelliJ API, but I managed to put together a testing
approach that works using real files and with limited mocking.
In the test below, the file src/test/testData/completion/text-file.txt
is a real file that I've created for testing
purposes and the makeInlineeCompletionRequest
function provides a convenient API to create a InlineCompletionRequest
object from a file path and offsets:
class ServiceTest : BasePlatformTestCase() {
// Where I'm getting tests files from, allow me to use relative paths in the tests.
override fun getTestDataPath() = "src/test/testData/completion"
// Create an inline completion for tests.
fun makeInlineCompletionEvent(): InlineCompletionEvent {
return object : InlineCompletionEvent {
override fun toRequest(): InlineCompletionRequest {
throw UnsupportedOperationException("event.toRequest() is not supported in tests")
}
}
}
// Create an inline completion request for tests.
fun makeInlineCompletionRequest(filePath: String, startOffset: Int, endOffset: Int): InlineCompletionRequest {
val event = makeInlineCompletionEvent()
val file = myFixture.configureByFile(filePath)
val editor = myFixture.editor
val document = myFixture.getDocument(file)
return InlineCompletionRequest(event, file, editor, document, startOffset, endOffset)
}
fun testId() {
val service = Service()
assertEquals("completamente", service.id.id)
}
fun testIsEnabled() {
val service = Service()
assertTrue(service.isEnabled(makeInlineCompletionEvent()))
}
// This function is more about knowing I can write and run tests than about testing the service.
fun testGetSuggestion() {
val request = makeInlineCompletionRequest("text-file.txt", 0, 0)
val service = Service()
val suggestion: InlineCompletionSuggestion = runBlocking { service.getSuggestion(request) }
val variants: List<InlineCompletionVariant> = runBlocking { suggestion.getVariants() }
assertEquals(1, variants.size)
assertEquals(1, runBlocking { variants.first().elements.count() })
assertEquals("Hello World!", runBlocking { variants.first().elements.first().text })
}
}
The tests are not currently testing much, but I'm getting acquainted with the IntelliJ API, and I need to nail the basics before moving on.
The tests are currently passing. Progress.
Next
Keeping the llama.vim code to the side, I will incrementally reproduce the implementation approach in my plugin.
The next step will be connecting to an external HTTP completion API to obtain my first real completion.