Blog Infos
Author
Published
Topics
, , , ,
Published

This article was inspired by the one from Jaewoong Eum, that describes how to exclude irrelevant class imports from auto-complete suggestions.

I was once designing a public API for a library product that needed to be convenient for both Kotlin and Java clients. If you write in Kotlin, you may know that for data classes the compiler generates lots of stuff for us, including componentN() functions for destructuring primary constructor parameters.

Destructuring is a Kotlin language feature that allows you to assign multiple variables at once from a single source. Say, if you have val pair = Pair("value1", 42), you can then call val (a, b) = pair. It’s possible when a class has operator functions component1()component2(), etc., no matter if they’re its own or extensions.

So, these auto-generated functions in data classes are rather implicit, and if you work with a data class in Kotlin, everything looks ok:

But that’s how it looks from Java:

But that’s how it looks from Java:

Additional componentN() functions in Java auto-complete suggestions that are not present in Kotlin files

It was even one of the reasons why, back then, I replaced public API data classes with regular classes that explicitly override kotlin.Any methods, preventing redundant methods from cluttering code completion. If you’re struggling with the same behavior – here’s some good news! You can download my plugin from JetBrains Marketplace, please feel free to add it to your Android Studio or IntelliJ IDEA, and share your feedback!

And if you’re curious about its internals, read on below.

This particular functionality is pretty easy to implement. First, when you create a plugin in your IDEA, lots of things, as it usually happens, are done for you under the hood. You click New Project and then select IDE Plugin from the Generators section. Now we are interested in 2 files – build.gradle.kts and plugin.xml.

The first is a standard build configuration file, there you can adjust versions if needed. There is a generated placeholder for plugin dependencies (plugins.set(listOf(/* Plugin Dependencies */))), let’s specify them: plugins.set(listOf("java", "org.jetbrains.kotlin")) and sync.

The second consists of metadata that is used by JetBrains Marketplace. When you fill it, please be sure to check the guidelines.

Inside the <extensions> we add the following:

<completion.contributor
        language="JAVA"
        implementationClass="your.package.KotlinDataClassCompletionContributor"/>

your.package.KotlinDataClassCompletionContributor is a path to our custom implementation of CompletionContributor we’re going to create a bit later.

If anything is highlighted in red, please sync the project.

Next to the already generated <depends> block, we add up the dependencies as we did in build.gradle.kts in previous steps:

...
<depends>org.jetbrains.kotlin</depends>
<depends>com.intellij.modules.java</depends>

And actually it’s crucial. If you, say, don’t specify the Java dependency here, everything works fine when debugging, but when you upload your plugin to the Marketplace, you encounter this error on verification:

By the way, as for debugging. It looks pretty interesting here. When you run the Run Plugin configuration, it starts a new instance of IDEA (in our case 2023.2.6, as specified here, though I work in 2024.2.1), but with our plugin enabled.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kobweb:Creating websites in Kotlin leveraging Compose HTML

Kobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.
Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kob ...

Jobs

Now let’s get to the core – implementing the CompletionContributor. You can review the full source code here, it’s relatively concise and straightforward. I’ll focus on the most interesting part – how to actually find generated operator functions componentN() to filter them out.

And now the plot twist… They are neither operators nor functions.

Let’s consider, say, the following data class:

data class User(val firstName: String, val lastName: String) {
    operator fun component3() = "$firstName $lastName"
}

And test it with our implementation. Let’s take the beginning of the shouldFilterOut function and add the log right after:

val psiElement = lookupElement.psiElement as? KtLightMethod ?: return false
val ktOrigin = psiElement.kotlinOrigin ?: return false
println("psiName = ${psiElement.name}; originName = ${ktOrigin.name}; $ktOrigin")

What we’ve done here:

  • filtered PSI Elements that are a KtLightMethod. It’s a type that represents Kotlin functions and properties in a way Java can understand, that is useful for code analysis, refactoring, and auto-completion purposes
  • filtered those which have the original Kotlin PSI element

Then we see something interesting in the logs:

psiName = component3; originName = component3; FUN
psiName = getFirstName; originName = firstName; VALUE_PARAMETER
psiName = getLastName; originName = lastName; VALUE_PARAMETER
psiName = component1; originName = firstName; VALUE_PARAMETER
psiName = component2; originName = lastName; VALUE_PARAMETER

Interestingly, both getters, as well as component1() and component2() functions, are represented in the PSI as VALUE_PARAMETER, despite being callable as functions in Kotlin. This representation suggests that these elements are directly tied to the constructor parameters.

Now, here’s an important note: despite what we see in the PSI, these auto-generated componentN() functions are still technically functions in Kotlin. It’s just that the PSI represents them differently, making them look more like parameters than functions.

However, you may note that our explicit component3()representation is different from the ones of the auto-generated componentN() functions. Moreover, they’d still be different even if it was operator fun component3() = firstName, i.e. pointed right to a value parameter.

And that’s good news for us! When you call the User class from Kotlin, the explicit componentN() function is not being filtered out:

component3() has been declared explicitly and is not filtered out from Kotlin auto-complete suggestions

 

As well as it isn’t with our implementation.

The rest is pretty straightforward – we simply check that the incoming entity is from a data class, that it’s a KtParameter instance, and that its name satisfies the regex for names containing only component followed by a positive number:

...
val isParamInDataClass = ktOrigin.containingClass()?.isData() == true && ktOrigin is KtParameter
if (!isParamInDataClass)
    return false
return componentNRegex.matches(psiElement.name)

You’re unlikely to collide with a real method thanks to naming conventions.

Please feel free to install my plugin, share your feedback, and support the repository with a star if you like it!

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu