Честно говоря, не знаю, нужно ли ставить тэг «перевод» на собственную статью.
Ну ок, поставил.
Всем привет! Недавно я наткнулся на статью, где описывается, как можно убрать мешающие варианты из автокомплита в Android Studio. Этот способ касается только классов — с методами у меня так же не получилось, и тогда мне пришла идея.
Как-то раз я дизайнил публичный API Kotlin-библиотеки, чтобы клиенты на Java могли пользоваться ей бесшовно, как и клиенты на Kotlin (ну, насколько это возможно). Если вы используете Kotlin, то, возможно, знаете, что для data-классов компилятор кучу всего генерирует за нас, в том числе функции componentN()
для деструктуризации параметров primary-конструктора.
Деструктуризация это фича языка, благодаря которой можно объявить сразу несколько переменных, производных от одного источника. Допустим, если у нас есть val pair = Pair("value1", 42)
, мы можем вызвать val (a, b) = pair
. Это работает, когда у класса объявлены функции-операторы component1()
, component2()
, и т.п., причем как member, так и extension.
Так вот, такие сгенерированные функции в data-классах как бы неявные, и если вы взаимодействуете с data-классом из Kotlin, то всё хорошо:
А из Java это выглядит вот так:
И это одна из причин, почему я тогда заменил data-классы публичного API на обычные с переопределенными методами из kotlin.Any
, благодаря чему всё это полотно со скрина выше пропало. Если вы тоже так хотите — у меня хорошие новости! Можно скачать мой плагин с JetBrains Marketplace, добавить его себе в Android Studio или IntelliJ IDEA, похвалить поделиться конструктивным фидбэком!
А если вы хотите сами сделать такой же, го под кат.
На самом деле такую функциональность сделать довольно просто. Во-первых, когда вы создаете плагин в IDEA, куча всего, как оно обычно и бывает, сразу же создается под капотом. Нажимаем New Project и затем выбираем IDE Plugin в секции Generators. Тут нам интересны 2 файла — build.gradle.kts и plugin.xml.
Первый — стандартный конфигурационный файл сборки, в нем вы можете поправить цифры в версиях, если нужно. Сразу заметим сгенерированный плейсхолдер для зависимостей плагина (plugins.set(listOf(/* Plugin Dependencies */))
), давайте их укажем: plugins.set(listOf("java", "org.jetbrains.kotlin"))
и синхронизируем проект.
Второй содержит метаданные для маркетплейса. Если будете заполнять, нужно будет свериться с гайдами.
Внутри тэга <extensions>
добавим следующее:
<completion.contributor
language="JAVA"
implementationClass="your.package.KotlinDataClassCompletionContributor"/>
your.package.KotlinDataClassCompletionContributor
это путь к нашей кастомной реализации CompletionContributor , которую нам еще предстоит создать.
Если IDE выдает ошибку и что-либо выделяет красным, синхронизируем проект.
Рядом с уже сгенерированным для нас блоком <depends>
добавляем зависимости, как мы делали это на предыдущем шаге в build.gradle.kts
:
...
<depends>org.jetbrains.kotlin</depends>
<depends>com.intellij.modules.java</depends>
И это суперважно. Допустим, если вы не укажете тут зависимость от Java, при дебаге у вас всё соберется и даже будет работать, но потом вы загрузите плагин в маркетплейс и на верификации получите вот такую ошибку:
С дебагом тут, кстати, тоже интересно. Если вы запустите конфигурацию Run Plugin, откроется новый инстанс IDEA (в данном случае 2023.2.6, отсюда, хоть у меня стоит 2024.2.1), но уже с включенным плагином.
Теперьк главному — реализуем CompletionContributor. Посмотреть сорсы можно тут, там немного и всё довольно тривиально. Перейдем сразу к самому интересному — как найти сгенерированные operator fun componentN()
и убрать их из выдачи.
А теперь plot twist… Это не operator и не fun.
Давайте рассмотрим вот такой data-класс:
data class User(val firstName: String, val lastName: String) {
operator fun component3() = "$firstName $lastName"
}
И проверим его на нашей реализации. Возьмем начало функции shouldFilterOut
и добавим логи:
val psiElement = lookupElement.psiElement as? KtLightMethod ?: return false
val ktOrigin = psiElement.kotlinOrigin ?: return false
println("psiName = ${psiElement.name}; originName = ${ktOrigin.name}; $ktOrigin")
Что мы сделали:
отфильтровали PSI-элементы, которые являются
KtLightMethod
. Это тип для представления Kotlin-функций и пропертей в понятном для Java виде, что полезно для анализа кода, рефакторинга, а также автокомплитаотфильтровали среди них те, у которых есть Kotlin PSI-элемент
И тут мы в логах видим кое-что интересное:
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
Примечательно, что оба геттера, как и функции component1()
и component2()
, представлены в PSI как VALUE_PARAMETER
, хоть и вызываются в Kotlin как функции. Возможно, такое представление связано с тем, что эти сущности напрямую связаны с параметрами конструктора.
Warning
Теперь важный момент: хоть мы и наблюдаем такое неожиданное поведение на уровне PSI, сгенерированные функции componentN()
технически всё так же остаются функциями. Мимикрируют под value-параметры они исключительно для PSI.
Однако вы могли заметить, что представление явной функции component3()
отличается от тех сгенерированных componentN()
. Более того, оно бы отличалось, даже если б функция была объявлена как operator fun component3() = firstName
, т.е. явно указывала на value-параметр.
И это хорошо! Когда мы используем класс User
из Kotlin-кода, явные функции componentN()
не отфильтровываются:
И, по удивительному совпадению, в нашей реализации точно так же!
Далее всё довольно просто — мы проверяем, что штука из саджеста пришла к нам из data-класса, что она является инстансом KtParameter
, и что ее название проходит регулярку для строк, содержащих только слово component и следующее за ним натуральное число:
...
val isParamInDataClass = ktOrigin.containingClass()?.isData() == true && ktOrigin is KtParameter
if (!isParamInDataClass)
return false
return componentNRegex.matches(psiElement.name)
Крайне маловероятно, что вы таким образом отфильтруете какую-нибудь важную функцию, спасибо конвенции имен.
Буду рад, если установите мой плагин, поделитесь фидбэком и поставите звезду на репозитории! Спасибо!