#35: IntelliJ IDEA: самый популярный редактор для Java
Специальный выпуск про устройство IntelliJ IDEA: Александр Пахомов расспрашивает разработчика JetBrains Даниила Овчинникова о том, из чего собран IDE изнутри. Разбирают разделение на платформу и плагины (поддержка Java — тоже плагин), `PSI` как абстрактное синтаксическое дерево «на стероидах» и пайплайн его построения (`Lexer` → парсер → `AST` → `PSI`), устройство автодополнения через вставку скрытого идентификатора `IntelliJ IDEA RULES`, модель экшенов и гетерогенный контекст, инкрементальный репарсинг, многослойное тестирование (парсинг, инспекции, quick fix, completion, property-тесты на `JetCheck`), а также вечный спор о форматах конфигурации (`XML` против `JSON`/`YAML`/`HOCON`) и планы вынести UI на Compose Multiplatform.
Главное
- В IntelliJ IDEA всё поделено на платформу и плагины: платформа — это общая, языконезависимая логика (например, сам экшен «показать документацию»), а всё языкоспецифичное (включая поддержку Java) вынесено в плагины.
- `PSI` (Program Structure Interface) — это абстрактное синтаксическое дерево «на стероидах»: параллельная структура над `AST`, обогащённая семантикой (ссылки с методом `resolve`, типы), по которой IDE подсвечивает ошибки несовместимости типов.
- Пайплайн построения дерева в Java-плагине: рукописный `Lexer` регулярками бьёт текст на токены, рукописный парсер собирает из них `AST`-ноды, а поверх `AST` строится `PSI`.
- Автодополнение работает через вставку скрытого идентификатора `IntelliJ IDEA RULES` в копию файла: он даёт в дереве ссылку, которую можно `resolve` и предложить все имена в текущем скоупе.
- Репарсинг инкрементальный: при вводе перепарсивается не весь файл, а только ближайший блок кода до первых фигурных скобок, поэтому автодополнение и подсветка работают быстро даже в больших файлах.
- Экшен — это объект, который знает, как выполниться (по сути лямбда) и как показать себя в UI; по нажатию шортката в keymap находятся все подходящие экшены, каждый решает, доступен ли он в текущем контексте (гетерогенной мапе), затем применяются промоутеры.
- В IntelliJ почти нет чистых юнит-тестов: тесты поднимают приложение и проект в памяти; отдельные фикстуры тестируют парсинг (текст → дерево), инспекции и quick fix (`Alt+Enter`), completion, а `property`-тесты на своей библиотеке `JetCheck` проверяют, что репарс блока даёт тот же результат, что и парсинг файла целиком.
- Текстовые форматы конфигурации (`XML`) выбирают, чтобы избежать class-loading: вынос всего class-loading из UI-потока в фон сделал стартап IDE почти мгновенным; `XML` предпочтён `Kotlin`-DSL (не требует загрузки классов) и `YAML` (где `no` — Норвегия — парсится как `false`).
В выпуске
- Даниил Овчинников — Разработчик в JetBrains, работает над API для языковых плагинов и межъязыкового взаимодействия в IntelliJ Platform; раньше активно развивал плагин для Groovy. GitHub ↗
Ссылки
Расшифровка
[00:00] Александр: Здарова! Меня зовут Саша Пахомов, и я инженер, который любит своё дело. Вы слушаете специальный выпуск подкаста про IntelliJ IDEA. Сегодня мы погрузимся внутрь этого софта и разберёмся, из чего он состоит и какие возможности предоставляет. Поехали!
[00:33] Александр: Разобраться с тем, как работает идея, мне поможет мой друг Даня Овчинников. Привет, Даня!
[00:39] Даня: Привет! Если открыть на GitHub Community-версию идеи и посмотреть на статистику контрибьюторов, то ты добавил 310 строчек кода в репозиторий и 247 тысяч удалил.
[00:48] Александр: Это очень много. Для сравнения: я закоммитил всего 22 тысячи строк кода в базу данных, которую разрабатываю, а вся кодовая база SQLite — это примерно 120 тысяч строк кода. То есть Даня написал почти три SQLite. Мне кажется, ему точно есть о чём рассказать нам про то, как работает IntelliJ IDEA.
[01:17] Александр: У тебя была интересная история про GitHub и contribution. Расскажи, пожалуйста, поподробнее, что ты имел в виду.
[01:23] Даня: История такая, что очень хорошо, что нам не платят за строчки кода.
[01:28] Александр: Это, кстати, да.
[01:30] Даня: Очень много людей, которые очень много коммитят строчек кода, делают какие-то большие массивные рефакторинги.
[01:40] Александр: Например?
[01:41] Даня: Например, когда у нас публикуется какой-то кусочек open source, там нужно пройти и проставить копирайт. Копирайты раньше были строчек 10–15 кода. Потом мы поменяли темплейт копирайта, и теперь копирайт — одна строчка кода. И если копирайта не было, раньше добавлялось 15 строк, сейчас добавляется одна. А если копирайт был добавлен в тот момент, когда было 15, то автоматическое обновление копирайта стирает эти 15 и добавляет одну. И всё это считается как строчки кода в GitHub. Чем больше файлов ты потрогал, тем больше строчек ты поменял — даже если поменял какой-нибудь пробел.
[02:25] Даня: Я хочу рассказать про семантические изменения. У нас было несколько очень больших коммитов про конвертацию анонимных классов в лямбды, когда мы мигрировали на восьмую Java. Примерно в то же время какие-то явные дженерики, которые стали выводиться в Java, тоже нужно было постирать: мы добавили инспекцию, которая подсвечивала нам лишнее, и применили её ко всему проекту.
[02:56] Александр: Интересно, а как такие вот большие изменения — например, конвертацию всей кодовой базы, хотя бы и по частям, из анонимных классов в лямбды — как проконтролировать, что ничего лишнего не накоммитилось в этот момент? Ведь может быть бага в софте или просто чихнул на клавиатуру, что-то упало — и это же может попасть в кодовую базу. Глазами-то не отследишь.
[03:17] Даня: Да, для этого нужно иметь достаточное покрытие тестами. У нас порядка 300 тысяч тестов гоняются на каждый коммит.
[03:26] Александр: Так, прикольно. Давай про тестирование чуть попозже — мне реально просто это интересно, я такой гик тестирования. А кто эти все изменения делал и сколько у него строчек кода? Если у тебя 310 — это ты делал или кто-то другой?
[03:40] Даня: Топ-1: 2 миллиона 196 тысяч строк добавлено и 1 миллион 375 тысяч удалено.
[03:48] Александр: Это капец как много.
[03:51] Даня: Да, это очень много. А это наш топ-контрибьютор, он работает с 2004 года.
[03:56] Александр: Офигеть. Прям огромный респект, это охренеть как круто. Особенно когда всё это, знаешь, вроде работаешь-работаешь, а потом… ну, есть на что посмотреть. Пусть это, понятно, не очень удобная и релевантная метрика, но всё-таки хоть какая-то есть — она показывает, что ты не просто так всё это время что-то делал. Я вот по себе скажу: когда смотрю, сколько выпусков подкаста я уже сделал, вроде бы делаешь-делаешь, раз в неделю выкладываешь, а потом смотришь — уже 33 выпуска вышло. И думаешь: ну вот эти 33 выпуска сделать — так-то довольно большой труд, но ты его как-то не замечаешь, пока делаешь. А потом посмотреть на результат — это вдохновляет и даёт надежду, что не всё просто так делается.
[04:43] Даня: Да, возможно, статистически имеет какой-то смысл смотреть на общую картинку. Но делать выводы по строчкам кода и как-то говорить, кто как перформит, — это, наверное, идея плохая.
[04:58] Александр: Все, кому не лень, уже обсудили, что строчки кода, как в целом и тест-каверадж, — наверное, не самый лучший показатель. Но да, про тесты чуть позже.
[05:12] Александр: Даня, я бы хотел немножко разобраться в базовых, основных структурах и компонентах в идее. И у меня к тебе такой вопрос. У меня есть IntelliJ IDEA, установленная на MacBook, и там слово «платформа» никак не фигурирует. Но если я попытаюсь написать плагин, то там будет слово «платформа». Расскажи, пожалуйста, что такое платформа и какое отношение она имеет к плагинам.
[05:38] Даня: Платформа в двух словах — это общая часть всех продуктов, которые построены на её базе. Другими словами, платформа — это всё, что не плагин. То есть у нас есть разделение на плагин и платформу.
[05:53] Александр: С точки зрения такой базовой, основной архитектуры IntelliJ IDEA.
[05:58] Даня: Да. Поддержка языка Java в том числе — это тоже плагин. Хотя его не видно в списке плагинов IntelliJ IDEA и его нельзя отключить, внутри это такой же плагин.
[06:07] Александр: Так, то есть вот абсолютно всё — это плагин. А что не плагин, что в себя включает платформа?
[06:15] Даня: Платформа — это общая часть, общая логика всех экшенов. Например, экшен, который показывает документацию: он находит в контексте documentation target и спрашивает у него — вычисли мне, пожалуйста, контент, который нужно показать в pop-up. Вот эта логика — тоже часть платформы. А имплементация, которая рендерит Javadoc, чтобы его можно было показать в pop-up, — это уже языкозависимая часть, она приходит из плагина, например из Java.
[06:43] Александр: То есть всё, что включает в себя язык программирования и какое-то его отображение, его автокомплит, — это плагин. Почти всё, что можно вынести в конкретные вещи, вынесено, а всё, что нельзя, остаётся в платформе.
[06:58] Даня: Или наоборот: всё, что можно вынести в платформу, что можно пошарить между другими языками, — уносится в платформу, а всё, что зависит от имплементации конкретного языка, остаётся, собственно, в плагине этого языка.
[07:12] Александр: Окей. А плагины как-то между собой разделяются, или есть просто плагин? Ну, например, любой ли плагин могу написать я как внешний контрибьютор?
[07:21] Даня: Все API, которые доступны публично, можно использовать в плагине и встраиваться туда. В этом, на самом деле, есть большая проблема.
[07:30] Александр: Какая?
[07:30] Даня: Что иногда внешние плагины встраиваются туда, куда мы не думали, что они встроятся.
[07:37] Александр: Например?
[07:38] Даня: Недавняя история про голосовые сообщения в комментариях.
[07:42] Александр: А, кстати, да, я в Твиттере видел этот кринж. Расскажи, как это вообще выглядит?
[07:46] Даня: У нас есть API, которое позволяет предоставить компонент для так называемых inlay. Inlay — это что-то, что можно нарисовать в редакторе на какой-то позиции или между какими-то строчками. И человек пошёл и написал такой плагин, который рендерит кнопочку play, позволяющую воспроизвести сообщение, лежащее в папке того же самого проекта. А изначально inlay задумывались, чтобы показывать type hint, например. Или кнопочку, что у такой-то декларации столько-то использований, при нажатии на которую эти использования показываются.
[08:27] Александр: То есть эта логика, это API предоставляется платформой для того, чтобы использоваться как type hint — когда я что-то делаю, и мне подсвечивается in place. Или, например, когда я делаю Command и навожу — Command или Control — на элемент, всплывает окошечко типа usage.
[08:48] Даня: Не совсем. Есть фича, которая называется CodeVision: она отрисовывает в редакторе над декларацией количество использований, или кто эту декларацию добавил, — то есть она берёт информацию из гита.
[09:03] Александр: А, вот это написано — автор, короче: вот я, Саша Пахомов, я это закоммитил. Вот эта маленькая надпись серенькая — это как раз та функциональность, в которой сделано API.
[09:11] Даня: Да-да-да.
[09:11] Александр: И какой-то чувак взял этот API и зарендерил там вот этот player.
[09:17] Даня: Да.
[09:17] Александр: А что… подожди, я думал, что ты предоставляешь текст, чтобы это зарендерить.
[09:22] Даня: Если бы мы дизайнили это API, мы бы сделали так, чтобы можно было предоставить текст.
[09:27] Александр: Так, а что там сейчас можно предоставить?
[09:30] Даня: Свинговый JComponent. И в этом JComponent ты можешь делать что угодно.
[09:35] Александр: То есть там условно YouTube можно в себя включить и смотреть?
[09:39] Даня: Да.
[09:39] Александр: Подожди, вот кто-то решил это заиспользовать?
[09:41] Даня: Да. А там, кстати, была какая-то issue в YouTrack, если я правильно помню всю историю. Появился мем, потом он оброс апвоутами на Reddit, пришли люди, сделали тикет. В комментах этого тикета люди пришли шутки шутить. А потом пришёл человек и сказал, что готов прототип, а через сутки опубликовал плагин.
[10:04] Александр: Что ты думаешь про это?
[10:06] Даня: Я думаю — класс. Человек увидел проблему, пошёл и решил. Это достойно.
[10:13] Александр: А в чём проблема?
[10:16] Даня: Проблема в том, что мы не задумывали такое использование inlay. И, скорее всего, оно будет кушать какое-то лишнее количество памяти.
[10:26] Александр: А сам use case — ну понятно, что это мем, там смешно или не смешно, — но есть ли у тебя личное мнение на тот счёт, нужны ли голосовые сообщения для кода?
[10:36] Даня: Да, у меня есть мнение. Я считаю, что не нужны.
[10:39] Александр: Я его полностью поддерживаю. Кроме шуток, они не нужны — там будут просто записи в четыре часа утра, когда человек читает код и плачет. Вообще мне кажется, что голосом объяснять сложные штуки… я как подкастер могу сказать — это очень сложная задача. Оно превращается в какие-то рассуждения, мямли, и человеку со стороны вообще непонятно, о чём речь, контекст теряется. Это реально сложно.
[11:07] Даня: Да.
[11:07] Александр: В тексте ты хотя бы потом его прочтёшь и поймёшь: какую-то фигню я тут написал, — и перепишешь.
[11:13] Даня: Хотя бы кружочки.
[11:15] Александр: Хотя бы кружочки. Или, знаешь, как тиктоки с подписями внизу — чтобы субтитры были: и прочитать можно, и посмотреть, и прослушать.
[11:24] Даня: Ну, посмотрим. Мне кажется, мем с субтитрами…
[11:26] Александр: Мем с кружочками или тиктоками реально тоже станет правдой. Вообще это отличнейший, прям канонический пример того, во что может вылиться протёкшая абстракция. По-хорошему тут бы показывать текст или какой-то специальный элемент, который не позволяет рендерить всё, что угодно.
[11:45] Даня: Да, или текст с иконкой.
[11:48] Александр: Текст с иконкой, да. Какую-то DTO-шку туда засунуть, с мета-информацией, а рендерить её будет уже идея. А там, получается, реально кусок UI высвечивается, и можно отрендерить всё, что угодно.
[11:59] Даня: Да.
[12:00] Александр: Класс, на самом деле, мне прям понравился этот пример.
[12:07] Александр: Так, мы говорили про платформу. Вопрос такой: а вот PSI — это часть платформы?
[12:13] Даня: Да. Набор интерфейсов — это часть платформы, а их имплементации отдельные, в каждом языке свои.
[12:21] Александр: PSI — я расшифрую для слушателей — это Program Structure Interface. Как я его понимаю? Это абстрактное синтаксическое дерево на стероидах.
[12:30] Даня: Да.
[12:30] Александр: Это такая древовидная структура — можно в голове представить абстрактное синтаксическое дерево. Вообще абстрактное синтаксическое дерево — это что? Например, когда программа, её сорцы, представляется в виде родителей и списка детей; у детей могут быть тоже дети, и так далее — древовидная структура. Например, класс — это родитель, а его филды и методы — дети. У метода — тоже свои дети: наверное, входные и выходные параметры, и так далее вниз-вниз-вниз. Такая древовидная структура представления программы. Но это больше как абстрактное синтаксическое дерево. А если мы говорим про Program Structure Interface, то оно обрастает дополнительной семантикой, причём семантикой языкоспецифичной или даже Java-специфичной — или редакторо-специфичной: какие-то нужды редактора, которые нужны от дерева, тоже могут присутствовать в PSI. Даня, я очень быстро и, возможно, сумбурно это описал. Что скажешь — прав я или нет? Что дополнишь?
[13:32] Даня: Да, ты прав. Мало чего дополнить есть, на самом деле.
[13:39] Александр: Смотри, Даня, тогда такой вопрос. А можешь привести какой-нибудь пример дополнительной семантики, которой обрастает абстрактное синтаксическое дерево в контексте PSI?
[13:49] Даня: Да. Ссылки в коде. Например, у нас есть Java-метод, у него есть тело, у тела — список стейтментов. Стейтменты бывают if, for и, например, expression-стейтменты. Экспрешены бывают: сложение, вычитание, чтение поля или вызов метода. И, например, если взять доступ к полю, то на идентификаторе, который ссылается на поле, будет существовать в PSI ссылка. У этой ссылки будет метод resolve, при вызове которого можно получить поле, на которое эта ссылка, собственно, ссылается. А у поля, в свою очередь, будет тип. Так мы подсвечиваем ошибки. Например, слева у нас стоит ссылка на локальную переменную типа Runnable, а справа — сейчас, я хочу сформулировать по-человечески — справа есть ссылка, мы её резолвим, видим, что там поле, берём тип, а он типа int. И понимаем, что int нельзя присвоить к Runnable, и подсвечиваем ошибку в редакторе.
[14:55] Александр: То есть, смотри, получается, что у меня есть метод, в нём есть какие-то наборы экспрешенов, стейтментов и так далее, и вот одно из них — это Runnable runnable = ..., и я туда хочу присвоить i из внешнего цикла. С точки зрения абстрактного синтаксического дерева оно построится и в целом скажет: окей, я могу так существовать. Но в контексте именно Java-семантики у нас типы не совпадают — нам нужно это провалидировать, показать ошибку, зарезолвить. Эту функциональность в абстрактное синтаксическое дерево добавляет PSI.
[15:36] Даня: Да.
[15:40] Александр: И именно поэтому он и существует — чтобы делать такие штуки. Я подразумеваю, таких штук можно придумать вагон и маленькую тележку — таких кейсов огромное количество. Ладно, тогда следующий вопрос. А как PSI, этот элемент, строится из сорцов? Вот есть у меня просто текстовый файл — Java, Markdown, любой. Как он превращается в дерево?
[16:00] Даня: В пайплайне несколько стадий. В самом начале на входе есть текст, он разбивается на токены лексером. На выходе Lexer у нас есть поток токенов, у каждого токена есть тип и range — где он начался, где закончился. Этот поток токенов уходит в парсер, который из этих токенов по их типам сочиняет дерево. На выходе из парсера мы получаем AST-ноды, а затем по этим AST-нодам создаются PSI. PSI — это параллельная структура над AST-деревом.
[16:35] Александр: Я понял. А когда ты говоришь про тип токена — что ты имеешь в виду?
[16:39] Даня: Это просто константа. Например, если мы говорим про Lexer, то это будет открывающая фигурная скобка, закрывающая фигурная скобка, пробел, перенос строки, точка с запятой, идентификатор, точка, keyword. Дальше этот стрим токенов улетает в парсер, и он сочиняет, например, из двух токенов — открывающей и закрывающей фигурной скобки — method body, если он сейчас пытается парсить method body в конкретный момент. Если на топ-левеле, в состоянии ноль, мы нашли keyword с типом class, за которым идёт идентификатор, за которым — открывающая и закрывающая фигурная скобка, то мы говорим, что вот эта нода с четырьмя детьми будет AST-нодой с типом class.
[17:32] Александр: Это представление AST в PSI или это представление потока токенов в AST?
[17:39] Даня: Это представление потока токенов в AST.
[17:42] Александр: То есть это тот шаг, который мы делали: лексер разбивает на токены, парсер составляет AST, AST потом превращается в PSI.
[17:51] Даня: Да.
[17:51] Александр: А давай на примере какого-нибудь элементарного файла. Допустим, у меня там package com.pakhomov, импорт java.lang.System.out, и я делаю public static void main, класс, и там пишу System.out.println("Привет, Даня"). Вот можно его сейчас распарсить так, мысленно?
[18:11] Даня: Очень легко. Но я бы его лучше сначала разлексил, потом распарсил. Там получается порядка сотни токенов — хочешь ли ты это слушать. Keyword package, затем пробел, затем идентификатор, точка, идентификатор, точка, идентификатор — название пакета, — которое кончается точкой с запятой, перенос строки, ещё раз перенос строки, слово import, пробел, идентификатор, точка, идентификатор, точка, идентификатор, точка — импорт кончился, — дальше перенос строки, ещё раз перенос строки, keyword class, пробел, название класса — идентификатор, — пробел, открывающая фигурная скобка, перенос строки, перенос строки, два или четыре пробела… [ускоренная тарабарщина] …закрывающая фигурная скобка, перенос строки, закрывающая фигурная скобка, перенос строки, конец файла.
[19:26] Александр: Это было охрененно. Я подумал, что это может быть отличным началом подкаста в немножко ускоренном виде.
[19:33] Даня: Да, ты можешь это ускорить и вот так оставить. Офигенно.
[19:36] Александр: Окей, это произошла работа лексера, правильно? Он их разбил.
[19:40] Даня: Да.
[19:40] Александр: У лексера какая-то настройка есть? То есть он настраивается, я так понимаю, каким-то языкоспецифичным файлом.
[19:46] Даня: Да.
[19:48] Александр: Там набор, наверное, каких-то регекспов, указываются кейворды и вот это вот всё.
[19:54] Даня: Не совсем. Когда открывается файл в IDEA, мы должны понять, какой у него тип. Вычисление типа файла происходит в несколько этапов, я не буду их описывать. На выходе у нас есть то, что это файл Java, потому что у него, скорее всего, расширение .java. После того как мы нашли file type, мы спрашиваем по этому file type: дай нам, пожалуйста, как тебя парсить. И получаем на выходе парсер. Дальше мы берём этот парсер, раскармливаем туда текст, и дальше всё происходит внутри парсера. Внутри парсер сам делает лексер, и на выходе у него уже готовое AST-дерево. Внутри может быть разделение на лексер и парсер, а может его и не быть. Например, ANTLR: если язык поверх ANTLR, то он сразу даёт тебе дерево как есть, потому что в ANTLR лексер и парсер — это одно.
[20:45] Александр: Прям книгу с драконом я вспомнил. Я её читал, интересно.
[20:49] Даня: Да. В Java конкретно есть отдельный лексер, который регекспами матчит токены и скармливает это в свой парсер, который возвращает дерево.
[20:57] Александр: То есть он не генеренный?
[21:00] Даня: Да, он рукописный, конечно.
[21:02] Александр: А есть пример генеренных парсеров, которые используются в Java?
[21:03] Даня: Да, у нас много генеренных парсеров. У нас есть Grammar-Kit, который позволяет написать BNF-нотацию и получить сгенеренный лексер, сгенеренный парсер и сгенеренные имплементации PSI.
[21:17] Александр: А, даже так? Но это, я так понимаю, кастомные какие-то генераторы?
[21:21] Даня: Свои, идеевские. Grammar-Kit он называется. Это наш плагин, который мы всем советуем, если кто-то начинает писать свой языковой плагин.
[21:29] Александр: Круто. Мне, на самом деле, вообще не представить, как можно написать генератор парсера.
[21:33] Даня: А там несложно.
[21:36] Александр: Несложно?
[21:37] Даня: Там каких-то 5000 строк кода.
[21:39] Александр: Ну, мне кажется, а что, сложно написать свой парсер? Типа, зачем его генерировать? Я всегда задавался вопросом.
[21:44] Даня: Нет. Если посмотреть на то, как написаны парсеры, там очень-очень много повторяющихся действий, и генератор позволяет тебе не думать о специфике — о том, как обрабатывать ошибки, например, в парсинге, — а просто писать правила, которые позволяют получить на выходе дерево. Можно посмотреть на сгенеренные парсеры, посмотреть, что там происходит, и окажется, что очень-очень много одинакового происходит на каждом этапе. А почему парсеры у нас свои? Наши парсеры умеют восстанавливаться. Например, если ты не дописал какой-нибудь стейтмент в Java, мы дойдём до первого переноса строки и начнём парсить следующую строчку, как если бы она была написана с самого начала.
[22:27] Александр: То есть он не выпадет с эксепшеном и не скажет: у меня лаг?
[22:31] Даня: Да. Вот именно это я и хотел спросить.
[22:33] Александр: Он ставит специальную ноду с ошибкой в дерево и пойдёт парсить дальше, как если бы стейтмент был закончен без ошибки. Эта нода потом в редакторе подсветится таким красивым красненьким подчёркиванием. И, например, в том месте, где это красненькое подчёркивание, в зависимости от того, какое там распарсилось дерево или кусочек дерева, можно будет либо completion позвать, либо вызвать complete statement, который автоматом тебе ставит закрывающие скобки и точку с запятой.
[23:05] Александр: Вот я сейчас слушаю — блин, выглядит как куча всякой прикольной информации, которую можно было бы запихнуть на вход Copilot. Ведь я сейчас думаю, что Copilot, на самом деле, консюмит сорцы — ну, я предполагаю. Вряд ли он всю эту информацию в виде дерева, в виде проектной структуры потребляет сейчас, по крайней мере. Мне кажется, было бы прикольно такой кастомный Copilot, который может всё это понять и на основе этого предложить тебе автодополнения — не на основе language large моделей вот этих. Ну ладно, это вообще другая тема. На самом деле мне очень понравились эти рассуждения про парсеры. Начинались-то они с чего? Как строится PSI? И вроде как я даже немножко понял, как он строится. Вопрос следующий. Вот, допустим, оно построилось — на примере даже того нашего маленького Java-класса, который ты распарсил в голове (удивительно). Что я могу с этим классом сделать, имея PSI под руками, — вот как разработчик плагина? Кстати, что будет наверху этого дерева, класс или файл?
[24:10] Даня: PSI-файл. И потом там уже будут дети. PSI-файл — это общий интерфейс из платформы. В случае Java там будет Java-файл. И у него я уже могу позвать: дай мне, пожалуйста, классы, дай мне импорт-стейтменты, дай мне название пакета.
[24:29] Александр: То есть я уже оперирую не на уровне AST, где есть child и parent, а могу сказать: дай класс, дай пакет.
[24:37] Даня: Да.
[24:37] Александр: Прикольно.
[24:39] Даня: И это часть имплементации PSI для Java.
[24:43] Александр: Да, я как раз в контексте парсера. Я тоже иногда увлекаюсь — не то чтобы увлекаюсь, а просто хочу, например, написать парсер для command-line-аппликейшена, для SQL, чтобы я писал SQL, и он парсил, и я мог у него спросить: а что дальше? Вот парсеры по дефолту вроде бы не могут на это ответить. Они как бы: сорян, я могу распарсить. Ну, сгенеренный — точно, мне кажется. А вот completion в идее мне интересно в плане автокомплишена и парсера. Автокомплишен — это совсем другая штука? Или парсер как-то про него знает и может какую-то залипуху ставить, типа здесь stop, вот completion?
[25:23] Даня: Completion в идее работает хитро. Для того чтобы было что комплитить, мы вставляем идентификатор в текст. Ты его не видишь, но он существует где-то в памяти.
[25:33] Александр: Что такое идентификатор? Ну то есть точка, куда надо что-то закомплитить?
[25:37] Даня: Да, например, какое-нибудь название переменной. У тебя есть название переменной, и ты хочешь получить все возможные другие названия переменной в этой позиции. У тебя уже есть идентификатор — и мы говорили про ссылки, про то, что ссылка резолвится в поле или в локальную переменную; и для того, чтобы она резолвилась, она должна существовать в дереве. Если её нет — если ты написал новую строчку, начал новую строчку, поставил enter, в конце предыдущей получил новую строчку, вызвал completion, — куда мы должны резолвить и что? Там нечего.
[26:13] Александр: Правильно.
[26:13] Даня: Для этого мы вставляем сгенеренный идентификатор, который в тексте не видно. Мы делаем копию файла, вставляем его туда, эта копия проходит через лексер и парсер, мы получаем новый кусочек дерева, поверх которого берём ссылку — в том месте, где мы начали печатать, где вызвали completion. И пытаемся эту ссылку также порезолвить. Соответственно, у нас теперь есть место, и мы можем порезолвить все возможные имена, которые есть в этом скоупе, и предложить их пользователю. И этот идентификатор выглядит как IntelliJ IDEA RULES.
[26:54] Александр: Могу я первый раз за всё время нашего разговора сказать, что это звучит как костыль?
[27:00] Даня: Нет, это… Расскажите мне, Александр, как бы вы это заимплементировали?
[27:05] Александр: Я базы данных разрабатываю, я вопрос хочу задать, мне интересно.
[27:10] Даня: Окей. Это не звучит как костыль.
[27:12] Александр: Ну то есть перепарсинг вот этот, пересбор всего — он вообще ничего не занимает?
[27:17] Даня: По памяти или CPU ты имеешь в виду?
[27:20] Александр: Да-да-да.
[27:22] Даня: Он ничего не занимает, потому что мы не перепарсиваем весь файл целиком, на самом деле. Мы поднимаемся в первый код-блок: то есть если ты начал тайпать внутри какого-нибудь бранча, то мы поднимемся до первых фигурных скобок и перепарсим только это.
[27:38] Александр: Теперь я понял. То есть, по сути, парсинг такой инкрементальный снизу.
[27:42] Даня: Да.
[27:42] Александр: И поэтому, понятное дело, что это работает. Потому что я подумал: каждый раз в больших файлах, если бы я печатал и оно вызывалось бы на каждый мой чих, — я подумал, как это вообще работает. Теперь понятно. Ну тогда беру свои слова обратно — это не костыль.
[27:55] Даня: Смотри, тут на самом деле два варианта. Мы могли бы брать честное дерево, которое существует на тот момент, но тогда правила, по которым completion-вариант нужно показывать или не показывать, были бы гораздо сложнее. По факту мы делаем матчинг дерева на какую-то структуру. Например, если у нас есть идентификатор, за которым пробел, за которым ничего, и мы стоим в этом месте, то мы должны попробовать предложить имена переменной, потому что, скорее всего, это декларация новой переменной. Если у нас есть идентификатор, за которым ничего, то это, скорее всего, ссылка — давайте предложим локальные переменные или филды, которые есть в скоупе. Если у нас нет ничего, то нам тоже стоит предложить локальные переменные и филды, потому что это может быть ссылка, пользователь может начать тайпить ссылку. Но, видишь, мы уже одно и то же предлагаем в двух случаях. Поэтому мы просто вставляем кусочек какого-то текста, просто чтобы он существовал в дереве, чтобы матчить вот эти кейсы — чтобы что-то было в тексте, на что можно поматчить.
[28:59] Александр: Ну, условно, такое решение проблемы «ничего»: то есть когда я уже начал печатать, там-то уже как бы текст есть.
[29:07] Даня: Да.
[29:07] Александр: И там уже просто берётся условно start with и пошёл поиск.
[29:11] Даня: Да.
[29:11] Александр: Всё, я понял. Прикольно. А вообще звучит вся эта логика как довольно матёрая эвристика — типа, код сложный. И есть ли там какие-то наметки на то, чтобы использовать, например, некоторые модельки, machine learning, чтобы они понимали, что нужно подсказать, а не код?
[29:32] Даня: Есть.
[29:34] Александр: Спасибо за ответ. Я понял.
[30:13] Александр: Кстати, экшен предоставляется платформой или это тоже какая-то специфика плагина, не знаю?
[30:18] Даня: Смотря какой экшен. Экшены могут предоставляться платформой, могут предоставляться плагинами.
[30:24] Александр: Ну давай пример экшена из платформы.
[30:27] Даня: Показать документацию к Java-классу, например. В любом контексте, на самом деле — в любом контексте, где есть так называемый documentation target, этот экшен будет работать.
[30:38] Александр: То есть, условно, я в Python вызываю какой-то метод у класса, и у меня там в definition этот метод поддокументирован, — то экшен show documentation его покажет. Потому что в целом можно абстрагироваться от специфики языка и сказать, что есть какой-то объект, у этого объекта есть документация, и дальше платформа это резолвит, где-то хранит, предоставляет.
[31:02] Даня: Больше того, экшены работают не только в редакторе. Они работают в том числе в Project View, если ты навигируешься по файлам.
[31:10] Александр: То есть это вот эта колоночка слева?
[31:12] Даня: Да. Ты можешь выделить какой-нибудь файл, нажать экшен и получить себе документацию. Например, превью картинки, если файл — это какая-нибудь иконка.
[31:22] Александр: Или голосовое сообщение — послушаю, что мне скажет этот класс. Прикол. А пример экшена, который языкоспецифичный, то есть не платформа?
[31:35] Даня: Я могу сказать так: пример экшена, который плагиноспецифичный.
[31:39] Александр: Давай.
[31:41] Даня: Fetch. Git fetch, который делает фетч из гита.
[31:44] Александр: Это моя самая часто используемая штука. Я делаю Shift-Shift и пишу «git fetch».
[31:48] Даня: А зачем ты пишешь «git fetch»? Можно просто писать «fetch».
[31:53] Александр: Потому что, по-моему, пару лет назад она мне делала фетч другое: либо что-то начинала искать, либо какой-то ещё есть фетч в идее — что-то fetch, типа не гит. И не туда я попадал. И я сделал себе привычку писать «git fetch», и теперь он всегда попадает в git fetch.
[32:10] Даня: Можно так, да. Экшенов бывает…
[32:13] Александр: А какие экшены — давай топ-3 экшена, которые ты используешь в day-to-day разработке?
[32:22] Даня: Command-Shift-A — это навигация к экшену, это тоже экшен.
[32:27] Александр: Да, это тоже экшен, конечно же. Сейчас голова начинает взрываться немного.
[32:32] Даня: И Command-E — это recent files.
[32:35] Александр: Command-E — это recent files. Кстати, да, походу, вот Command-E я прям использую часто.
[32:41] Даня: Экшенов много. Например, открыть тул-окно с коммитом, с локальными ченджами — это тоже экшен, Command и какая-нибудь цифра. Или открыть какое-нибудь другое тул-окно. Когда ты нажимаешь Enter в идее — это экшен. Когда ты нажимаешь Enter в редакторе — это экшен. Когда ты нажимаешь Escape — это экшен. Когда ты в терминале нажимаешь Escape и возвращаешься в редактор. И в то же время git fetch — это тот же самый интерфейс.
[33:13] Александр: Так, давай допустим, экшен — это такой поток вообще событий в софте, который пользователь как-то…
[33:22] Даня: Не совсем. Экшен — это что-то, у чего есть то, что знает, как выполниться (то есть лямбда по факту), и то, что знает, как себя представить в UI: текст, дескрипшн, иконка — презентация. Всё. А теперь, когда ты нажимаешь какой-нибудь шорткат, мы в keymap находим все экшены, которые имеют такой шорткат, и спрашиваем у всех: доступны ли они в текущем контексте. Каждый экшен может решить, доступен он или нет. Соответственно, если экшен говорит, что доступен, мы применяем к ним промоутеры, то есть реаранжим их, берём первый и вызываем его.
[33:59] Александр: А контекст что всё хранит?
[34:01] Даня: Контекст — это мапа из какого-то абстрактного ключа в что угодно. Большая гетерогенная мапа. Тут хитро. Если экшену нужен текущий файл, то проси текущий файл. Если текущий файл будет открыт в редакторе, ему вернётся файл из редактора. Если фокус будет стоять в дереве с файлами или в каком-нибудь другом тул-окне, которое может сказать, что у меня есть, — разные тул-окна, разные системы предоставляют его. Соответственно, контекст — это гетерогенная мапа: мы идём по иерархии компонентов вверх, у каждого компонента начинаем спрашивать, что в тебе есть, каждый компонент отдаёт, что у него есть, и из-за этого мы собираем базу, снапшот. После этого начинают работать правила. Как я и сказал, если у нас есть текущий редактор, он скажет, что есть в контексте. Но экшен просит файл — а у нас есть правило, которое говорит, что если кто-то спрашивает файл, а у нас есть редактор, то мы вернём тот файл, который открыт в этом редакторе. Такое разрешение и индирекция. У нас получается много разных контекстов — в смысле, вместо декартова пересечения «экшен отдельно» мы строим один контекст один раз, и все экшены на него нападают.
[35:17] Александр: Я понял, да. Это довольно челленджовая с точки зрения проектирования кода штука — чтобы это всё быстро работало и выглядело логично. Я понимаю, что если бы я это решал, то первый блин у меня вышел бы, наверное, комом — чтобы это как-то работало, а люди приходили такие: «а, ну понятно, как это работает, я вот сейчас что-то хочу добавить — добавлю это сюда».
[35:44] Даня: На самом деле у нас было какое-то количество точек расширения, extension point’ов, для AspectJ.
[35:52] Александр: Да, я хотел спросить: а есть ещё примеры подобных плагинов, штук, которые надо?
[35:56] Даня: Да, вот в них влез Lombok изначально, чтобы как-то поддержку добавить. А потом уже что-то было сделано специально, и это было плохо. А потом Lombok переехал к нам и стал забандленным плагином.
[36:08] Александр: Забандленный плагин — это тот, который мне не нужно скачивать из Marketplace, он вместе с сорцами идеи поставляется.
[36:16] Даня: Да.
[36:16] Александр: А почему так?
[36:18] Даня: Потому что так проще. Потому что каждый релиз у нас что-то разваливалось. Нам проще держать сорцы Lombok у себя в монорепе.
[36:26] Александр: Наличие Lombok в бандле, я подразумеваю, просто упрощает поддержку и, в частности, тестирование. Например, когда ты тестируешь новый релиз идеи, проще протестировать его, когда он в части сорцов.
[36:38] Даня: Когда он в части сорцов — ключевой момент. Потому что мы могли бы его и не бандлить. Почему он бандлится — у меня нет ответа, это к продакту вопрос.
[36:47] Александр: Ну окей, про Lombok, я думаю, понятно наше отношение к нему — моё личное. Я, по-моему, даже про это где-то писал или говорил, что я против Lombok. Хочешь билдер — напиши его руками, попроси сгенерировать идею. Хочешь геттер — попроси сгенерировать идею. Хочешь toString, хочешь hashCode — ответ тот же самый.
[37:07] Даня: Или используйте датаклассы.
[37:10] Александр: Да, или используйте Kotlin, если хочется сахарку. Ну, про Kotlin, на самом деле, я не эксперт, не знаю — я бы не хотел углубляться в Kotlin, потому что я на нём долго профессионально, скажем так, не писал. Я писал только свои какие-то проекты; у меня мнение не профессионального разработчика, а дилетанта, и делиться им я пока не готов. Я-то профессионально не пишу, но подразумеваю, что ты так или иначе на Kotlin пописываешь — может быть, даже фуллтайм, а может быть, фуллтайм плюс ещё полставки. Скажи, сколько Kotlin сейчас в идее, насколько его много? Потому что изначально-то она на Java была написана.
[37:51] Даня: Kotlin очень много, и последний год мы рекомендуем весь новый код писать на Kotlin. Если нужны какие-то правки — плюс-минус не одна строчка, скажем так, а нормальный такой патч на код на Java, — то мы советуем сконвертить сначала класс в Kotlin. Сейчас есть тулза, которую ты сканываешь под папку, и она тебе даёт этот формат. Но эта тулза ничего не знает: у нас есть куча тест-даты, у нас есть специальные сорцы, поверх которых мы гоняем свои тесты — правильно ли оно распарсилось, вот такой вход, такой вход. И эта тулза ничего не знает про то, что сорцы в тест-дате лежат — их надо заигнорировать. И у нас таких тест-дат в каждом модуле. То есть нельзя так написать, что вот эту папку не бери — там этих папок жопой жуй.
[38:39] Даня: Kotlin у нас достаточно много.
[38:40] Александр: Ну давай так: больше половины или меньше?
[38:44] Даня: Меньше.
[38:44] Александр: Окей.
[38:46] Даня: Пока. Я думаю, это достаточно.
[38:50] Александр: У кого есть какие вопросы — Kotlin или Java, — я думаю, всё и так понятно. Java больше даже в IntelliJ IDEA. Ладно, это была шутка.
[39:01] Даня: У Kotlin есть одно преимущество: можно использовать корутины.
[39:05] Александр: А как насчёт green threads, которые приходят и всё никак не придут?
[39:13] Даня: Так вот, да, они всё никак не придут. На самом деле недавно был релиз, и мы сейчас переезжаем на JDK 21. Но не ради green threads, а просто потому, что мы в какой-то момент очень долго сидели на какой-то старой JDK и не переезжали. А теперь мы стараемся переезжать на LTS как только, так сразу.
[39:33] Александр: Постоянные переезды. Мне кажется, с каждым годом всё проще и проще. Хотя некоторые вещи любят дропать — например, SecurityManager, по-моему, грозятся дропнуть. Иногда он бывает полезен.
[39:44] Даня: И слава богу.
[39:44] Александр: Да, но некоторые его, наверное, используют. Или ты можешь использовать библиотеку, которая его использует, — и что делать тогда?
[39:50] Даня: Она будет работать, но он будет ноуп: то есть всё разрешено по дефолту, и библиотека не сломается. А security, которая на него полагалась… если security полагалась на SecurityManager, значит, оно было дырявое, его в любом случае как будто бы и не было.
[40:10] Александр: Ну да, я согласен. Это довольно уже такая легаси-херня, никому не нужная, поэтому его и дропают.
[40:23] Александр: Смотри, на самом деле мы уже довольно много поговорили, и я бы не хотел перегружать с точки зрения этой функциональности ни себя, ни слушателей. Мне хотелось бы задать последний вопрос, который будет довольно большой. Это про то, а как это всё тестируют. Вот это выглядит как супер-сложные штуки. И как это тестируется? Расскажи, вот как тебе хочется, так и расскажи.
[40:47] Даня: В IntelliJ практически нет юнит-тестов. Давай так: практически все тесты поднимают приложение и открывают проект в памяти.
[40:56] Александр: Могу ли я их назвать интеграционными тестами?
[40:59] Даня: Интеграционными их тоже не назовёшь. Они юнит-тесты, но у них очень-очень жирная фикстура. Есть, например, языковые тесты. У нас есть отдельная фикстура для того, чтобы потестировать парсинг: на вход текст, на выходе дерево в текстовом формате. Есть тесты, которые тестируют лексер. На самом деле лексер и парсер тестируются вместе — нет смысла тестировать отдельно набор токенов. Тестируется сразу дерево на выходе целиком: если токены не матчатся, то и дерево не будет матчаться, потому что в этом же дереве написаны те типы токенов, которые вернул лексер. Дальше есть property-тесты, которые тестируют репарс. Репарс — это когда мы меняем что-то в блоке кода и поднимаемся до первого блока и перепарсиваем его, вместо того чтобы перепарсивать весь файл. Property-тесты тестируют, что репарс возвращает тот же самый результат на выходе, что и парсинг файла целиком.
[42:01] Александр: А property-тесты — давай подробнее, возможно, кому-то неизвестно, что это такое. Property-тесты — это штука, которая тестирует не какие-то конкретные контролируемые выходы на вход, а тестирует набор свойств. Например, если мы тестируем какой-нибудь array list, то утверждение такое: что бы мы туда ни добавили, isEmpty должно возвращать false. Соответственно, если мы добавляем что-то 10 раз, а потом то же самое удаляем 10 раз, isEmpty должно возвращать true. Вот это свойство — что лист пустой или не пустой — и тестируется. И framework умеет генерить набор рандомных операций, которые выполняются поверх структуры, и проверять свойства. То есть нужно проверить свойства под разными входными данными. А сам ты входные данные придумывать не будешь, потому что ты человек, ты можешь что-то не придумать. Framework генерирует набор входных данных. Я так понимаю, вы используете, наверное, что-то своё, потому что, по-моему, только одна библиотека property-тестинга есть на Java.
[43:04] Даня: Какая?
[43:05] Александр: Java… вот я забыл, но она прям, по-моему, одна.
[43:09] Даня: Мне очень интересно. У нас мы используем JetCheck — это наша библиотека.
[43:14] Александр: А ещё есть, знаешь, что это?
[43:16] Даня: QuickCheck, ты имеешь в виду?
[43:18] Александр: Ну, QuickCheck — это типа из Haskell какая-то штука пришла, её просто переписали на Java.
[43:23] Даня: Ну да, у нас своё решение называется JetCheck.
[43:27] Александр: Прикольно. А оно — я могу его использовать в своём проекте?
[43:29] Даня: Да, оно опубликовано как что-то внешнее, независимое.
[43:35] Александр: Класс.
[43:36] Даня: После property-тестов — так, у нас есть языковые тесты, есть… Ну, про property-тесты — это всё парсинг. Дальше есть хайлайтинг. Когда создаётся какая-нибудь новая инспекция, есть специальный формат, в котором описываются ожидания от этой инспекции. Например, ты пишешь класс и специальными тегами расставляешь, что вот здесь должна быть ошибка с таким-то текстом, или не ошибка, а предупреждение с таким-то текстом, после того как инспекция отработает. И для каждой инспекции мы пишем тесты, которые это проверяют.
[44:08] Александр: То есть у меня есть входной файл как вход на тест. В нём написан Java-класс, и где-то я делаю ошибку — например, делаю обратный слэш, который синтаксически не парсится в Java. Или какую-нибудь фигню пишу, и потом рядом специальным тегом, специальными символами указываю…
[44:26] Даня: Не совсем. Инспекция — это то, что, например, подсвечивает тебе возможный null pointer exception. Это не синтаксические ошибки.
[44:40] Александр: А, всё, я понял. То есть инспекция — это такой вот хинт.
[44:43] Даня: И в тексте теста будет написан Java-код, и какой-нибудь там if или доступ к полю, который может теоретически дать NPE. И тот рейндж, который у тебя был подсвечен в редакторе, обёрнут в тег, в котором написано, что это должен быть warning с таким-то message.
[45:06] Александр: Ну, вроде прям логично, всё понятно. Ещё какие?
[45:11] Даня: Тесты на какие-нибудь экшены с кодом. Например, инспекции позволяют рассказать, а что сделать с этим warning, как его починить. Например, поставить проверку на null: обернуть вот это место с warning в проверку на null. Называется quick fix, вызывается через Alt-Enter обычно. Есть тесты, которые после того, как прошёл хайлайтинг, — а теперь примени первый quick fix, который там был, и сравни с тем, что в итоге получилось. Соответственно, на входе у тебя текст, размеченный тегом, на выходе — текст после применения quick fix. Это тест, который тестирует от начала до конца. То есть, видишь, тест тестирует две штуки: то, что инспекция отработала и на самом деле показала предупреждение, и то, что это предупреждение можно пофиксить, починить именно так.
[46:12] Александр: Вообще это супер-логично — тестировать именно такой use case: он так и проходит — человек видит, нажимает, получает результат. И это покрывает весь тест.
[46:24] Даня: Есть тесты, которые тестируют просто fix. То есть там он не тестирует сообщения и наличие ошибки — ему всё равно, warning это было или error. Просто там есть место, в которое поставить нужно каретку, и в этом месте якобы как бы нажать Alt-Enter, взять фикс с таким-то названием и применить его. Есть разные тесты. Есть тесты, которые тестируют completion. Они выглядят как: есть файл, в нём специальным тегом указывается, куда надо поставить каретку, и вызывается метод — дай мне, пожалуйста, все completion-варианты в этой позиции. Дальше в тесте можно выбрать конкретный completion-вариант и применить его. Например, разные completion-варианты не только текст самого варианта вставляют — они могут вставлять скобочки, переносы строк, на самом деле что угодно могут вставлять. И тесты, которые тестируют наличие этого варианта и потом применение — что после применения текст стал выглядеть вот так.
[47:15] Александр: А вопрос: сколько всё это добро, хозяйство, занимает времени на исполнение?
[47:24] Даня: Мы используем сейчас подход такой, что разработчик пушит в мастер сразу через специальную конфигурацию на TeamCity. Это TeamCity прогоняет все тесты, которые у нас есть, в Aggregator. Aggregator — это специальная конфигурация на TeamCity. После того как тесты прошли, после того как ничего нового не упало — то есть кто-то предыдущий может сломать, но твой пуш всё равно должен пройти, ответственность на том, кто первый сломал, — после того как все тесты прошли, он попадает в мастер автоматически, роботом. Конечно, есть всякие проблемы, когда суперпозиция багов: у тебя всё хорошо, у меня всё хорошо, мы вместе запушили — всё сломалось. Но в общем процесс занимает час. У нас стоит тайм-аут на два часа, но мы стремимся к тому, чтобы это занимало час. Если какой-то тест не проходит, нам нужно перезапустить конфигурацию, потому что мы никогда не знаем, флаки это тест или нет; и у нас максимум два часа на попытку, максимум четыре попытки. В идеале ты просто ставишь на вечер пуш и уходишь, а с утра у тебя запушило.
[48:37] Александр: Короче, вопрос такой. Я когда писал свой плагин — ну и из документации видно, — что некоторая настройка плагина и его вписывание в жизненный цикл идеи происходит с помощью XML-конфигурации. Вот у меня такой вопрос: как ты думаешь, XML-конфигурация — это вообще хороший выбор для конфигурирования? И что бы ты выбрал сегодня, например, если бы писал всё с нуля?
[48:57] Даня: Если бы я выбирал формат сейчас, я бы выбрал XML.
[48:59] Александр: Почему?
[49:02] Даня: В XML есть всякие приколы типа инклудов. Например, можно инклудить один XML из другого — в JSON такого нет. С другой стороны, эти же самые инклуды, которые в XML прикольные, мы никак не можем выпилить, потому что мы на них столько говна нажрались.
[49:19] Александр: То есть тут хрен знает. А почему, например, с помощью Kotlin они конфигурируют? Я знаю, кто-то как-то конфигурирует.
[49:25] Даня: О, смотри, это хороший вопрос. Kotlin требует class-loading и выполнения. Вся суть текстовых форматов — избежать class-loading.
[49:42] Александр: Ну, оно там просто не надо.
[49:44] Даня: Нет, не «не надо», а просто когда у вас объёмы class-loading огромные и каждая штука тянет за собой каждую следующую, то этот class-loading на самом деле виден. Сейчас мы переписали стартап так, чтобы UI-поток занимался минимумом — только рендерингом и общением со свинговыми структурами, только тем, чем он должен был заниматься изначально. А всё остальное, весь class-loading, выкинули на бэкграунд. И стартап стал мгновенным. Нам пишут пользователи, говорят: я не вижу сплэш-скрин, покажите мне сплэш.
[50:20] Александр: То есть, давай я немножко так мысль сформулирую: можно сделать хороший user experience даже в UI, если нормально всё написать, — и независимо от того, что там Java. Нормально пишите — нормально будет.
[50:37] Даня: Нормально пишите — нормально будет.
[50:38] Александр: Мне кажется, это отличный вообще лозунг выпуска будет.
[50:41] Даня: Да. Про class-loading. Суть текстового формата — избежать class-loading. Суть XML — избежать кастомных библиотек, которые работают с JSON: в старой Java не было никаких возможностей работать с JSON.
[50:58] Александр: Вообще говоря, по мне так JSON — вообще отвратительный вариант для конфигурации, потому что, во-первых, там есть строковые литералы, которые просто бесят.
[51:09] Даня: Так а в XML тоже. CDATA — чем тебе не строковый литерал?
[51:12] Александр: Я не говорю, что по сравнению с XML, — я говорю, что в целом как язык конфигурации он должен быть довольно лаконичным. И каждый раз писать вот этот тип… То есть я должен в Vim комфортно открыть, поменять условную конфигурацию всю там на серваке, без схода, без передеплоивания — и чтобы оно как-то заработало. Ну, например, конфигурацию логирования — частый use case: ты заходишь на сервер, меняешь. Мне не хочется вот эти строковые штуки писать, мне хочется быстренько найти какое-то значение и поменять его. В этом плане удобно, конечно, property-файл, или какой-нибудь YAML, или HOCON. Вот, например, в Ignite 3 используется HOCON. А YAML… YAML должен умереть, простите меня, пожалуйста. Это проблема Норвегии — хотя бы ради этого он должен.
[52:00] Даня: Поясни нам тут из Армении проблемы Норвегии, с YAML непонятно.
[52:04] Александр: В YAML есть литералы, есть там какой-нибудь true-false, есть no. И no — это код Норвегии.
[52:14] Даня: Всё, я вспомнил, да-да.
[52:19] Александр: Про будущее, я так понимаю, в целом нет особого смысла спрашивать, потому что ты сказал, что это инсайты.
[52:24] Даня: Я могу сказать один инсайт. Мы сейчас работаем над тем, чтобы затащить Compose в идею.
[52:34] Александр: Docker Compose? Или Jetpack Compose?
[52:36] Даня: Нет. Compose for Desktop.
[52:39] Александр: Это на котором, я так понимаю, Toolbox работает?
[52:43] Даня: Да. Нам нужен Compose для того, чтобы можно было написать свой рендерер, который отправлял бы UI по сети, чтобы ты мог написать один раз формочку, работать с ней как если бы она была локально, но отображалась она где-то там.
[52:56] Александр: А где-то там — это… server-side rendering?
[52:59] Даня: Где-то там — это клиент ремоут-дева, да.
[53:04] Александр: Угу. И это умеет Compose?
[53:06] Даня: Compose — классное асинхронное API. Он не умеет это из коробки, но если весь UI писать так, то мы сможем написать свою имплементацию, подсунуть её хостовому IDE, так чтобы отправлять отрендеренные кусочки в клиентскую IDE.
[53:24] Даня: Про Compose хочу добавить: мы изучаем такую возможность. У нас уже есть способ отправлять UI, но мы бы хотели какое-то более-менее стандартное решение иметь. Мы изучаем этот вариант — возможно, мы от него откажемся. У нас уже есть какой-то прототип, который как-то работает. Мы пытаемся сделать на нём плюс-минус сложный UI, понимаем, что это заводится и этим удобно пользоваться, — почему бы и нет. Но это не 100%.
[53:53] Александр: Угу-угу. Но довольно неплохой такой прогноз, который стоит ожидать. А может быть, стоит и не ожидать — загадка. Прикольно. Даня, есть ли тебе что ещё рассказать? Или…
[54:07] Даня: Есть примеры, что ещё рассказывают?
[54:10] Александр: Какие примеры? Ты первый мой гость.
[54:12] Даня: Хорошо. Нормально делай — нормально будет.
[54:16] Александр: Мне кажется, это отличная мысль для того, чтобы завершить этот выпуск подкаста. Не забывайте делиться подкастом с друзьями и коллегами. Давайте прокачивать себя и людей вокруг. Ну а на этом всё. Услышимся!