IDE-плагин

Плагин MEL для Android Studio

Тул-окно «MEL» показывает MEL-экраны проекта как стейт-машины. Слева каталог экранов, справа на выбор: граф состояний, живое превью и отчёт о проблемах графа. Любой элемент открывает исходник. Вдобавок в редакторе работают 26 инспекций MEL.

О странице

Документация модуля :tools:mel-idea-plugin репозитория sbbol_android_mel (id ru.sberbank_sbbol.mel.idea, версия 0.1.3, к библиотекам ru.sberbank_sbbol.mel 0.2.0) для разработчика: как установить плагин, на каких студиях он работает, что умеет сейчас и что запланировано. Раздел «умеет сейчас» сверен с кодом: того, чего нет в коде, там тоже нет.

Что внутри (2) установка из репозитория плагинов или из исходника. (3) совместимость со студиями и риски внутренних API. (4) минимальный словарь, чтобы читать граф. (5) возможности по видам тул-окна, инспекции, извлечение графа. (6) что дальше, со статусами. (7) как устроено: выбранные технологии.

Источник истины по архитектуре MEL — код в :core:mel, затем спека. Учебное введение — руководство, правила detekt — mel-detekt.

Установка

Из репозитория плагинов

Плагин раздаётся не через JetBrains Marketplace, а из собственного репозитория плагинов. Это два файла по фиксированным адресам: zip плагина и дескриптор updatePlugins.xml. Оба лежат в папке mel-maven/mel-plugins репозитория sbbol_android_mel и раздаются как raw-файлы git-хостинга. Студия опрашивает дескриптор, ставит и обновляет плагин как обычный.

  1. Settings → Plugins → ⚙ → Manage Plugin Repositories → +, добавить raw-адрес файла updatePlugins.xml из папки mel-maven/mel-plugins репозитория sbbol_android_mel. Адрес — по месту. Репозиторий закрытый, поэтому при скачивании студия попросит аутентификацию.
  2. Во вкладке Marketplace найти MEL, установить и перезапустить студию.
  3. Открыть тул-окно MEL (по умолчанию пристыковано справа).
Репозиторий плагинов пока пуст Папки mel-maven/mel-plugins в репозитории сейчас нет: плагин туда ещё не выкладывался. Пока этого не сделали, ставьте zip с диска (см. ниже). Всё описанное на странице относится к версии 0.1.3 из исходника.

Сборка из исходника и установка с диска

JAVA_HOME=/usr/lib/jvm/java-21-openjdk ./gradlew :tools:mel-idea-plugin:buildPlugin
# → tools/mel-idea-plugin/build/distributions/mel-idea-plugin-0.1.3.zip

Нужен JDK 21. Сборка идёт против локально установленной Android Studio, дистрибутив студии не скачивается. По умолчанию берётся ~/.local/share/JetBrains/Toolbox/apps/android-studio, другой путь задаётся через -Pmel.androidStudioPath=… или ANDROID_STUDIO_PATH. Какую студию выбрать для сборки, сказано в разделе «Совместимость».

Zip ставится через Settings → Plugins → ⚙ → Install Plugin from Disk…, после чего нужен перезапуск. На Linux то же самое делает одна команда tools/mel-idea-plugin/update-plugin.sh: она собирает zip и распаковывает его в каталог плагинов самой новой ~/.local/share/Google/AndroidStudio*. Другой каталог задаётся через --plugins-dir=… или MEL_STUDIO_PLUGINS_DIR, а --restart перезапускает студию.

Выкладка новой версии

Сначала поднимите version в tools/mel-idea-plugin/build.gradle.kts: студия с уже установленным плагином видит обновление, только если версия выросла. Затем:

./gradlew melPublishPlugin -Pmel.outDir=mel-maven                     # собрать, положить в mel-maven/mel-plugins, закоммитить
./gradlew melPublishPlugin -Pmel.outDir=mel-maven -Pmel.push=true \
    -Pmel.publish.remote=<remote>                                       # то же и запушить

Задача melPublishPlugin собирает плагин, кладёт zip и updatePlugins.xml в трекаемую папку mel-maven/mel-plugins и коммитит её. В папке держится ровно один zip: старый удаляется, дескриптор ссылается на новый.

  • версия — из имени zip;
  • id, имя и since-build — из пропатченного при сборке plugin.xml;
  • базовый raw-адрес для ссылки на zip в дескрипторе — -Pmel.plugin.repoUrl=…;
  • «что нового» на экране обновления в студии — -Pmel.plugin.notes="…".

Альтернатива — raw-репозиторий на Nexus через tools/mel-idea-plugin/publish-plugin.sh: скрипт собирает zip, генерирует updatePlugins.xml и заливает оба HTTP PUT вместо коммита. Версию он умеет поднять сам (--bump или --version X.Y.Z, после этого закоммитьте build.gradle.kts), --dry-run только показывает дескриптор. Адрес репозитория задаётся через MEL_PLUGIN_REPO_URL: значение по умолчанию в скрипте — заглушка. Подробности обоих путей — в PUBLISHING.md репозитория sbbol_android_mel.

Где попробовать В sbbol_android_mel боевых экранов нет: это библиотеки и инструменты. Плагин открывают на проекте-потребителе. В sbbol (ветка develop) на MEL уже есть, например:
  • CreationPullerMachine (feature/creation) — часть переходов вынесена в функцию машины openDocument(…);
  • LightBankChargesPullerMachine (feature/charges) — ветвление в цели: next(if (…) Empty else Content(…));
  • BankSearchPullerMachine (feature/counterparties) и SelfRequisitesMachine (feature/payorder) — стейт-одиночка: состояние — data class без sealed-иерархии;
  • AccountRestrictionsMachine (feature/account) — sealed-граф с push.
Секционных экранов в develop пока нет.

Совместимость

ЧтоЗначение
Минимальная сборкаsinceBuild = 253.32098.37 (Android Studio AI-253.32098.37, IntelliJ Platform 2025.3). untilBuild не задан.
СборкаПротив локально установленной студии (local(…) в build.gradle.kts). Собирать стоит на студии минимальной версии: байткод, собранный против более новой, может ссылаться на API, которых в 253 нет. JDK 21. Из точек Analysis API с линией 261 (Kotlin 2.3.0) явно сверен sealedClassInheritors, остальные защищены откатом на эвристику (см. ниже).
Нужные плагины IDEKotlin, Java, Android (org.jetbrains.android), Android Design Tools (com.android.tools.design). Все они бандлены в Android Studio, на неё плагин и рассчитан.
Режим KotlinK2: supportsK2="true" в plugin.xml. Без этой пометки студия в K2-режиме считает плагин несовместимым.
Урок: внутренние API студии меняются между версиями

Превью рисуется через внутренний нестабильный API рендера Android Studio, и с API 261 он поменялся:

  • RenderTask.dispose() удалён, вместо него disposeAsync();
  • RenderResult.getRenderedImage() теперь возвращает RecyclableImage вместо ImagePool.Image.

Прямой вызов привязывает байткод к одной версии: собранный против одной студии плагин падал на другой с NoSuchMethodError, а исходник с disposeAsync() не компилировался против студии прежнего поколения. Поэтому оба вызова идут рефлексией: берётся тот метод, что есть в текущей студии, а getCopy() есть у обоих типов картинки. Рендер превью работает и до, и после смены API.

Вывод: sinceBuild без untilBuild от такой поломки не защищает. Компиляцию против целевой студии надо гонять как регрессию, но такой проверки в репозитории пока нет.

  • Резолвер типов. Если три точки Analysis API на другой сборке не найдутся (LinkageError, NoSuchMethodError), резолвер молча откатится на эвристику по именам. Граф всё равно построится, но часть целей может стать «?».
  • Превью вне read-action. Перечисление @Preview и рендер идут вне read-action в расчёте на поведение 2026.1. На других версиях это может падать.
  • Инспекции используют только стабильные API: LocalInspectionTool и Kotlin PSI. Их тесты гоняются на IntelliJ IDEA Community 2025.1.

Как читать граф: минимальный словарь

MEL описывает экран явной стейт-машиной в духе Elm, поэтому экран можно нарисовать конечным графом. Ниже ровно столько терминов, сколько нужно, чтобы понимать картинку плагина. Сама архитектура объяснена в руководстве: словарь State/Intent/Effect — в шаге 1, секции — в шаге 8.

ПонятиеВ кодеВ плагине
Состояниелист sealed-типа S из MelMachine<S, I, E>; если S — конкретный класс без sealed-иерархии, он сам единственное состояниеУзел графа. Цвет рамки показывает роль: зелёная — обычное, синяя — оверлей, янтарная — терминальное.
Фактыпараметры конструктора состоянияСтроки в теле узла (до шести, дальше «… +N»). Полный список — в инспекторе.
Интентлист sealed-типа IПодпись на ребре.
Клеткаon<S, I> { … }; onAny<I> — для любого состоянияРебро «состояние —интент→ цель». Если тело клетки или цель next/push ветвится, на каждую ветку рисуется своё ребро.
Операция стекаnext / push / pop / popTo / stay в теле клетки
  • next — обычная серая линия.
  • push — толстая синяя.
  • pop и popTo — пунктир назад к состоянию, сделавшему push. Если таких состояний несколько, пунктир ведёт к кольцу-выходу ◯.
  • stay и переход в себя на холсте не рисуются. Это «действия на месте»: их число показывает чип ↻N в шапке узла.
Командаcommand(…) / commandWithProgress(…) в переходеРебро зелёным пунктиром. Ключ и интенты-результаты показаны в инспекторе.
Подпискаsubscribe(key, active<S>(), …) { … }Строка со значком трансляции внутри узлов своего скоупа.
ЭффектMelEffect в переходеНа холсте не рисуется. Виден в инспекторе и на уровне клеток.
Стартstart() (startNext)Бейдж START на стартовом состоянии.
Оверлейoverlay<S> в MelScreenСиняя рамка и бейдж OVERLAY.
Терминальноенет исходящих переходов; выводится из графа, а не объявляетсяЯнтарная рамка и бейдж TERMINAL.
Недостижимоенет входящих переходов, и это не стартБейдж NO REACH.
Секциякласс Section<S, I, E>; клетки и подписки — в register()Чип ▸N на узле показывает число секций. Клик по чипу раскрывает их.
Неразрешённая цельцель next/push, которую не удалось вычислить статическиРебро в янтарный узел «?».

На узле показывается один бейдж в порядке приоритета: NO REACH → START → OVERLAY → TERMINAL.

Что плагин умеет сейчас

Всё ниже реализовано в исходнике :tools:mel-idea-plugin и :tools:mel-graph. Разделы идут по видам тул-окна, затем инспекции и внутренняя механика.

Как это устроено (одним абзацем) Плагин извлекает машины статически, без сборки проекта. Сервис MelScreenIndex по словарному индексу находит файлы, в коде которых встречается MelMachine, подтверждает наследника по PSI и собирает все .kt из поддерева каталога машины. Каждый пакет-экран разбирает MelGraphExtractor из :tools:mel-graph. Это чистый PSI, и его исходник компилируется прямо в плагин. Результат — ScreenGraph. Типы резолвит AnalysisApiTypeResolver на K2 Analysis API, при сбое — эвристика по именам. Из графа считаются производные (линтер, сводка, карта), и все виды рисуют одну и ту же модель.

Тул-окно и виды

ВозможностьЧто делает
КаркасТул-окно MEL справа. Слева каталог (≈ 28 % ширины), справа колонка с собственным тулбаром и сменными видами, внизу строка статуса: «N экранов · M скрыто», во время скана — «Сканирую проект…».
Переключатель видовСегменты Граф состояний · Превью · Проблемы. Четвёртый вид, «Карта экранов», собран, но выключен флагом (см. ниже). Выбранный вид и открытый экран запоминаются между сессиями, отдельно для каждого проекта.
Тулбар
  • Обновить — полный пере-скан.
  • Уменьшить / Увеличить / Вместить / 1:1 — управляют холстом графа.
  • Экспорт PNG и Копировать Mermaid активны только в виде графа.
  • Поле «Найти состояние…» видно только в виде графа.
Единая точка выбораОткрытый экран, выбранное состояние, вид и запрос хранит MelSelectionController. Панели не вызывают друг друга, а шлют ему намерения и слушают его уведомления. Поэтому состояние, выбранное на графе, сразу показывают инспектор и превью.

Каталог экранов

ВозможностьЧто делает
ОбнаружениеСловарный индекс отбирает только файлы, в коде которых есть слово MelMachine. Затем PSI проверяет супертип: прямой или транзитивный в пределах файла. Весь проект не парсится.
Группировка по пакетамВ группу экрана попадают все .kt из поддерева каталога машины. Каждый файл достаётся самому длинному пакету-машине-предку. Так контракт, оверлеи и секции из под-пакетов приходят к своему экрану без коллизий одноимённых состояний.
СписокВ строке: имя экрана без суффикса Machine/Screen, укороченное имя Gradle-модуля, метка test. В тултипе: пакет и число состояний, переходов и подписок. Боевые экраны идут раньше тестовых, внутри — по полному имени.
ФильтрыПоиск по имени, модулю и пакету. Выпадающий список модулей. Кнопка test показывает тестовые экраны: по умолчанию они скрыты, их число видно в статусе.

Граф состояний

ВозможностьЧто делает
РаскладкаELK 0.9.1: алгоритм layered (Sugiyama) сверху вниз, рёбра-сплайны, подписи размещает сам ELK. При любой ошибке ELK граф раскладывается встроенным алгоритмом (BFS-ранги от старта и барицентр) с прямыми рёбрами.
УзлыСкруглённые карточки на Java2D. Внутри: имя, до шести фактов имя: тип, подписки скоупа. В шапке справа: бейдж роли, чип ▸N секций, чип ↻N действий на месте, кнопка <> «к коду».
РёбраСтиль зависит от операции и команды (см. словарь). Подпись — интент, плюс debounce в мс, если он задан. Самопетли и stay на холст не выносятся.
Псевдо-узлы
  • «?» — неразрешённая цель next/push.
  • Кольцо ◯ — выход для pop, когда состояние, сделавшее push, не единственное, и для popTo без цели.
  • Если конец ребра — не объявленное состояние, рисуется узел-призрак.
Старт отмечен бейджем, отдельного кольца нет.
Зум по секциям
  1. Клик по чипу ▸N раскрывает секции состояния сеткой карточек. Клетки вне секций собраны в карточку «Ядро».
  2. Клик по карточке показывает клетки секции колонкой: операция и цель, команда с ключом, результаты, эффекты, debounce. Подписки идут отдельными карточками.
  3. Клик по клетке открывает код.
Наверх ведут хлебные крошки «Состояния › Content › Секция», клик по пустому месту и Esc. При смене экрана зум сбрасывается.
Подсветка
  • Поиск подсвечивает совпавшие узлы и приглушает остальные.
  • Выбранный узел получает синее свечение, несмежные с ним рёбра приглушаются.
  • Наведение подсвечивает узел и его рёбра, курсор над кликабельным элементом становится рукой.
Легенда и темаСворачиваемая легенда в правом нижнем углу показывает цвета ролей и стили переходов. Все цвета заданы через JBColor, поэтому граф следует светлой и тёмной теме IDE.

Инспектор

Панель справа от графа. Ширина задаётся сплиттером и запоминается. Это HTML-панель с упорядоченной палитрой, где каждая строка открывает исходник.

РежимЧто показывает
Ничего не выбрано Сводка экрана:
  • пакет и модуль;
  • шесть счётчиков: состояния, переходы, действия на месте, подписки, эффекты, терминальные;
  • предупреждение о недостижимых состояниях;
  • список состояний с бейджами, клик по имени выбирает состояние.
Выбрано состояние Четыре блока:
  • Факты.
  • Подписки: ключ → интенты.
  • Действия на месте: интент · cmd:ключ · эффекты · debounce, под строкой — интенты-результаты команды.
  • Переходы: исходящие (next/push/pop с целью, командой и эффектами) и, приглушённо, входящие, включая ▶ start.

Снять выбор можно кликом по пустому месту графа или клавишей Esc.

Превью

ВозможностьЧто делает
Живой кадрСлева кадр состояния, отрендеренный Layoutlib самой студии, без Gradle и screenshot-тестов. Модель строится так же, как у встроенного Compose Preview, поэтому параметры @Preview учитываются: устройство, API, uiMode, fontScale, locale. Справа упрощённый граф: только состояния и по одной стрелке на пару. Клик по состоянию показывает его кадр.
Подбор @PreviewПревью ищется в классах пакетов <пакет машины>.ui и самого пакета машины, в крайнем случае — в файле машины. Имя превью сравнивается с состоянием по убыванию приоритета: точное <State>Preview, суффикс …<State>Preview, просто содержит имя состояния. Нужен Android-модуль с фасетом Android.
Устойчивость
  • Рендер ждёт окончания индексации и делает до трёх попыток с паузой 800 мс.
  • Если кадра нет, повторяет рендер с противоположным режимом декораций устройства.
  • Кадры кэшируются и сбрасываются только при пере-извлечении графа экрана.
  • После Gradle sync кадры перерисовываются.
Нет кадраПоказывается одна общая заметка «@Preview не найден / рендер не дал кадр». Причины не различаются, а выбранный при неточном совпадении @Preview не показывается (это запланировано, см. «Что дальше»).

Проблемы (граф-линтер)

Вид показывает результат lintScreen из :tools:mel-graph. Это чистая функция над графом, покрытая тестами. Проблемы сгруппированы по категориям с числом находок. У каждой есть заголовок, объяснение и ссылка в исходник. Если проблем нет, вид пишет «Проблем не видно».

КатегорияКогда срабатывает
Недостижимые состоянияНет входящих переходов.
ТупикиНет исходящих переходов, а состояние не терминальное.
Неразрешённые целиЦель next/push/popTo не вычислилась статически.
Переходы в несуществующий стейтЦель разрешилась в имя, которого нет среди листьев.
Старт не определёнНет start() или его цель не разрешилась.
Результаты команд без клеткиКоманда рождает интент, который не обрабатывает ни одна клетка.
Подписки без onErrorТолько на падучем источнике. Молчит для горячего потока, для .catch выше по цепочке, для flowOf/emptyFlow и для шины listen*/observe*/.asFlow().
Подписки на горячем источникеMutableStateFlow, stateIn, shareIn и подобные. Если onError задан, отмечается, что он мёртв.
Эмиты подписок без клеткиПодписка шлёт интент, для которого нет клетки.
Подписки на несуществующий стейтСкоуп подписки ссылается не на лист.
Оверлеи без pushСостояние объявлено оверлеем, но его никто не кладёт в стек.
next в оверлей вместо pushnext ведёт в состояние-оверлей: заменит экран вместо наложения, вероятно, нужен push. Находкой сейчас считается и обновление оверлеем собственных фактов через next(s.copy(…)).
Интент с несколькими клеткамиОдна пара «состояние · интент» обработана в теле машины и в секции либо в двух секциях. Ветвление одной клетки находкой не считается.

Карта экранов (выключена)

Кросс-экранная карта навигации: по узлу на экран и по связи на каждый эффект навигации (по префиксу имени: NavigateTo…, Navigate…, GoTo…). Цель связи ищется по совпадению метки с отображаемым именем экрана. Если совпадения нет, связь остаётся «выходом наружу». Карта рисуется тем же холстом с паном и зумом. Клик по экрану открывает его граф, а инспектор показывает список связей.

Сейчас вид выключен флагом mapEnabled = false в MelToolWindowFactory.kt и в переключателе не появляется. Если в прошлой сессии была выбрана карта, при открытии тул-окно переключится на граф. Код готов (MelScreenMapView, MelScreenMapPanel), а включение ждёт проверки производительности на большом проекте.

Навигация в код и холст

ВозможностьЧто делает
Переход к исходнику Куда ведёт клик:
  • кнопка <> в шапке узла — к объявлению состояния;
  • ребро — к вызову перехода в теле клетки on<>;
  • клетка на уровне секций — к её коду;
  • строки инспектора и «Проблем» — к своему месту в коде.
Файл открывается через OpenFileDescriptor по SourceRef (файл и смещение).
ВыборКлик по узлу выбирает состояние, и инспектор показывает его детали. Клик по пустому месту или Esc снимает выбор, а на уровнях секций поднимает на уровень вверх. Новый экран открывается без выбора, со сводкой в инспекторе.
Пан и зумГраф двигается перетаскиванием, колесо масштабирует вокруг курсора в пределах 0,3×–3×. При открытии экран вписывается в окно, но не крупнее 1:1.

Экспорт

ВозможностьЧто делает
Экспорт PNGТекущий вид графа в масштабе 1:1 сохраняется в файл через диалог сохранения. На уровне секций сохраняется то, что на экране.
Копировать MermaidMermaidEmitter собирает flowchart graph TD и кладёт его в буфер обмена:
  • classDef по ролям;
  • стили рёбер: push — ==>, команда и pop — пунктир, stay — петля;
  • факты и подписки внутри узлов;
  • click-директивы.
JSON (в ядре)JsonEmitter из :tools:mel-graph выдаёт компактный JSON графа для программных потребителей. В тул-окне он не выведен.

Инспекции в редакторе

В plugin.xml зарегистрировано 26 localInspection для Kotlin: группа «MEL», уровень Warning, включены по умолчанию. На каждое правило detekt-набора :tools:detekt-rules приходится одна инспекция с тем же именем и суффиксом Inspection.

  • Проверки работают только на PSI, без type resolution, поэтому подсвечивают нарушения на лету в K2-редакторе.
  • Логика не дублируется: PSI-хелперы MelPsi.kt лежат в source-root, общем с detekt-правилами, так что эвристика у них совпадает.
  • Тесты — отдельный модуль :tools:mel-idea-inspections-test: 130 кейсов инспекций на лёгкой тест-платформе (IntelliJ IDEA Community 2025.1), без Android.

Quick-fix есть у двух инспекций. MelSubscribeOnError предлагает «Добавить onError = { … }» и вставляет заглушку с TODO. MelDataContractLeaf предлагает «Сделать data class», только если у класса есть параметры конструктора. Остальные 24 инспекции только подсвечивают нарушение. Что проверяет каждое правило, описано на странице mel-detekt.

Правило / инспекцияНазвание в IDE
MelSubscribeOnError · quick-fixПодписка MEL без onError (§4.2)
MelHotFlowSubscriptionSourceГорячий источник подписки MEL (onError мёртв)
MelBlockingInCommandOrSubscriptionБлокирующий вызов в команде/подписке MEL
MelNoManualEffectCollectionРучной сбор effects MEL
MelNoRouterCloseEffectПрямой router.close() в эффект-слое MEL
MelNoCoroutinesInMachineКорутины внутри машины/секции MEL
MelStatelessMachineМашина MEL должна быть stateless
MelNoSendInReducesend() внутри машины/секции MEL
MelNoRawFrameworkConstructorПрямой конструктор каркаса MEL (нужен DSL graph { })
MelCommandOnErrorMustReturnIntentthrow в onError команды/подписки MEL
MelNoThrowInReducethrow в reduce MEL
MelNoMutationInReduceМутация захваченного поля в reduce MEL
MelNoSideEffectsInReduceСайд-эффекты в reduce MEL
MelNoNonDeterminismInReduceНедетерминизм в reduce MEL
MelNoBlockingInReduceБлокирующий вызов в reduce MEL
MelImmutableFactsИммутабельность стейта/фактов MEL (§4.5)
MelNoInstrumentsInStateUI-инструменты в стейте/фактах MEL (§5)
MelEffectsAreDataЭффект MEL должен быть данными (§5)
MelNoMutableEffectContextИммутабельность эффект-контекста MEL (§5)
MelNoTypeChecksis/as по контракту MEL (§5)
MelDataContractLeaf · quick-fixЛист контракта MEL должен быть data
MelNoLambdaInContractЛямбда/функциональный тип в контракте MEL
MelNoPlatformTypesInContractПлатформенные типы в контракте MEL
MelNoBehaviorInContractПоведение (функции) в контракте MEL
MelSealedContractRootКорень контракта MEL должен быть sealed
MelMigrationRatchetНаследование старого MVI вне allowlist (§9)

Живые обновления

ВозможностьЧто делает
Правка кодаPSI-листенер следит только за физическими KtFile и после дебаунса 700 мс запускает лёгкий пере-скан. Кэш ведётся по пакетам: пересчитывается только пакет, у файлов которого сдвинулся штамп модификации. Правка чужого файла ничего не пересчитывает.
Gradle syncПосле синхронизации идёт полный пере-скан со сбросом кэша, и превью перерисовывается на свежем classpath.
Скан вне EDT Скан идёт неблокирующим read-action:
  • уступает write-action и потом перезапускается;
  • ждёт окончания индексации;
  • серия запросов схлопывается до последнего;
  • скан отменяется при закрытии окна.
Пока он идёт, каталог показывает спиннер.
Что переживает пере-сканЭкран восстанавливается по полному имени, а если его больше нет — берётся первый. Если граф открытого экрана не изменился, виды не перестраиваются, и зум, уровень секций и кадр превью остаются на месте. Выбор состояния держится, пока такое состояние существует.

Извлечение графа и резолвер типов

ВозможностьЧто делает
Ядро на чистом PSIMelGraphExtractor строит граф по уже разобранным KtFile и использует только org.jetbrains.kotlin.psi.*. В плагин компилируется его исходник против PSI студии. Тот же код без IDE гоняется в тестах :tools:mel-graph.
Что извлекается
  • Тип-аргументы MelMachine<S,I,E>.
  • Sealed-листья S с фактами и позицией в коде. Если S — конкретный класс без sealed-иерархии (стейт-одиночка, например data class), он сам единственное состояние, и цель любого next/push — он же.
  • start()/startNext, graph{}, on<S,I>, onAny<I>; ветки if/when в теле клетки дают по ребру.
  • Операция NEXT/PUSH/POP/POP_TO/STAY и её цель. Ветвление внутри цели (next(if (…) Empty else Content(…))) тоже даёт по ребру на ветку.
  • Хвостовой вызов функции, объявленной в той же машине или секции (openDocument(i.document)), разворачивается в переходы из её тела; рекурсия обрезается.
  • Эффекты; command/commandWithProgress с ключом и интентами-результатами.
  • config { debounce<I>(…) }.
  • Подписки: ключ, скоуп active<S>(), onError, испускаемые интенты, класс источника.
  • Оверлеи из overlay<S>. Терминальность выводится из рёбер.
Три формы — стейт-одиночка, ветвление в цели и переходы в функции — взяты с боевых экранов sbbol: без их разбора «Проблемы» давали бы там ложные недостижимые состояния, неразрешённые цели и переходы в несуществующий стейт. Формы покрыты TransitionShapesTest.
СекцииКлассы Section<S, …> находятся структурно, по всем файлам пакета. Тело register() даёт клетки и подписки с пометкой секции; клетка секции может быть и ссылкой на метод (::confirmSign). Поэтому секции видны и при подключении через DI, когда их имён в коде машины нет.
Резолвер типовТочка расширения — TypeResolver. В IDE её реализует AnalysisApiTypeResolver на K2 Analysis API: листья через sealedClassInheritors, цели через expressionType. При любом сбое, кроме отмены, он откатывается на NameHeuristicResolver: листья ищутся обходом иерархии в объединённом файле пакета, цель — по имени конструктора, ведущему идентификатору или типу параметра лямбды.
Best-effortНеразрешённое не выбрасывается: цель становится null, на графе появляется «?», а в «Проблемах» — строка. Отдельные парсеры (эмиты подписок, результаты команд, длительности) работают по сужениям, описанным в KDoc экстрактора.

Что дальше

Статусы:

  • в планах — поведение продумано по сути, в коде его ещё нет;
  • частично — часть уже есть, в карточке сказано, чего нет;
  • идея — кандидат на обсуждение.

Сделанное описано в разделе «Что плагин умеет сейчас», например инспекции в редакторе и инспектор.

Вид «Структура»

в планах

Экран в виде дерева «состояние → секция → клетки и подписки» рядом с графом. Секции делят между собой клетки, а не состояния, поэтому на секционном экране почти вся логика — действия на месте вокруг одного состояния: диаграмма показывает такой экран одним узлом, а список — клетками, сгруппированными по секциям. Синхронизация с кареткой в обе стороны: клик по строке открывает код, каретка в редакторе подсвечивает свои строки. Штатная Structure View не подходит: она привязана к одному файлу, а MEL-экран живёт в нескольких.

Польза: секционный экран читается списком, а место в коде сразу видно в модели экрана.

Quick-fix к идиомам MEL

частично

Есть у двух инспекций, для которых существует механически безопасный фикс: MelSubscribeOnError (добавить пропущенный onError = {…}) и MelDataContractLeaf (сделать data class, только при наличии параметров конструктора).

Остальные 24 только подсвечивают. Кандидаты на фикс: throw в reduce → эффект /ShowError, ручной collect потока → subscribe<>, сырой конструктор фреймворка → MEL-фабрика, is-проверка → клетка on<>.

Польза: нарушение лечится одним нажатием, поэтому идиоматичный MEL писать и переносить с v3 дешевле.

Маркеры в gutter на клетках

в планах

Маркер в полосе значков у on<S, I>, рождения машины и subscribe. Маркер клетки называет пару «состояние × интент» и ведёт к целевому состоянию. Неразрешённая цель получает отдельный маркер, который сообщает об этом, а не ведёт наугад. Вне машин и секций маркеров нет.

Польза: прямо в редакторе видно, что лямбда — клетка и куда она ведёт.

Навигация по контракту (Find Usages)

в планах

Переходы из контракта:

  • от состояния — к обрабатывающим его клеткам и к регистрации view или оверлея;
  • от интента — к клеткам и к местам отправки;
  • от секции — к её клеткам и подпискам.

Результаты берутся из извлечённого графа, а не из текстового поиска. Если у состояния нет view, это сообщается явно.

Польза: интент трассируется сквозь контракт, машину и view одним жестом.

Секции на графе: зум и линза

частично

Зум есть: модель знает секцию каждой клетки и подписки, чип ▸N раскрывает секции состояния, а секция раскрывается в клетки (см. граф).

Линзы нет. Задумано оставлять яркими выбранные секции и приглушать остальные без изменения раскладки, со сбросом при смене экрана.

Польза: на экране-концентраторе видна логика одной секции, а форма графа не меняется.

Честное превью

в планах

Различать «@Preview не найден» и «рендер не удался», называть состояние и показывать, какой @Preview выбран при неточном совпадении имени. Сейчас вместо этого одна общая заметка (Превью).

Польза: превью перестаёт молча подменять результат запасной заметкой.

Карта экранов в переключателе

в планах

Карта готова, но выключена флагом (см. выше). План:

  1. прогнать построение на большом проекте, проверить время и поведение при изменении дерева исходников;
  2. при необходимости сделать построение ленивым и отменяемым;
  3. снять флаг.

Польза: чтение на уровне проекта, а не одного экрана.

Вкладка миграции MVI v3 → MEL

в планах

Отдельная вкладка тул-окна. Она находит конструкции v3 (Mutator и хендлеры, Listen-эффекты, type-checks) и предлагает MEL-маппинг: хендлеры → on<>, Listen-эффект → команда или подписка, переформовка sealed. Примеры — как в mel-migration. В коде ничего нет.

Польза: ведёт по повторяющимся шагам переноса легаси-экранов, а это главный барьер внедрения.

Подсветка неразрешённых целей

частично

Есть: ребро в янтарный узел «?» на графе и категория «Неразрешённые цели переходов» в «Проблемах» с переходом в код.

Нет: особого стиля «висячего» ребра и подсказки, как сделать цель статически анализируемой.

Польза: границы статического анализа видны и подталкивают код к формам, понятным графу и ревьюеру.

Линты покрытия / мёртвых интентов

частично

Есть: «Проблемы» ловят недостижимые состояния, а также результаты команд и эмиты подписок, которые не обрабатывает ни одна клетка. На графе из этого видно только бейдж NO REACH.

Нет: поиска интентов контракта, которые не обрабатывает ни одна on<>. Модель графа не хранит список интентов контракта.

Польза: ловит неполные и устаревшие машины, которые компилируются, но содержат мёртвое или отсутствующее поведение.

Находки поверх графа

идея

Бейджи на узлах и рёбрах по находкам detekt и граф-линтера в их исходном диапазоне, с кликом в фикс. Сейчас находки линтера живут только в виде «Проблемы».

Польза: архитектурный линт связан с визуальной моделью, проблема видна там, где живёт нарушающее поведение.

Экспорт графа в docs-сайт

частично

Mermaid и PNG плагин уже отдаёт. Нет действия, которое кладёт диаграмму экрана сразу в формат сайта mel-docs, без ручного копирования.

Польза: опубликованная документация остаётся в синхроне с кодом без ручного сопровождения диаграмм.

Слой эффектов / команд / подписок

идея

Второй режим диаграммы: команды и подписки становятся узлами и связываются с состояниями, которые их взводят, и с интентами, которые они подают обратно. Ранний вид «Эффекты» был заменён «Проблемами». Сейчас команды, эффекты и подписки видны в инспекторе и на уровне клеток.

Польза: невидимый async-слой и циклы обратной связи становятся явными.

Minimap и focus-режим

идея

Minimap для больших машин и режим, гасящий всё, кроме выбранного узла и его соседей на расстоянии одного ребра. Сейчас выбор узла приглушает только несмежные рёбра, а поиск — несовпавшие узлы.

Польза: большие экраны остаются навигируемыми.

Интерактивный симулятор / степпер

идея

Задать стартовое состояние и кликать интенты: граф подсвечивает активное состояние, показывает факты результата, эффекты и интенты-результаты команд. Это сухой прогон по модели, без исполнения.

Польза: рассуждать о достижимости без сборки и тапов по живому экрану.

Git-дифф графа экрана

идея

Сравнить граф между двумя ревизиями и показать с цветовой разметкой добавленные и удалённые состояния и рёбра, ключи команд, debounce, эффекты, скоупы подписок.

Польза: изменения поведения видны в терминах ревьюера, а не стеной Kotlin-диффа.

Мастер нового экрана

идея

Действие New → MEL Screen генерирует контракт, скелет машины, Compose-view и проводку melViewModel. Это фаза письма: инструмент для написания экранов, а не для чтения.

Польза: новые экраны идиоматичны с первого коммита.

Live-templates / postfix MEL

идея

Шаблоны для частых клеток: on<S,I>{ … }, start(), subscribe<>(key){} с onError, блок команды с результатами, лист-состояние с фактами. Это тоже фаза письма.

Польза: ускоряет ручное письмо машин и навязывает правильную форму, например всегда предлагает слот onError.

Как устроено

Выбранные технологии по слоям. Плагин не объявляет graph-библиотек, кроме ELK, и не использует JCEF.

СлойЧто выбраноГде в коде
ИзвлечениеPSI самой студии и исходник :tools:mel-graph, подключённый как source-root: экстрактор, линтер, производные, карта, эмиттеры.tools/mel-graph/src/main/kotlin, MelScreenIndex.kt
Резолв типовK2 Analysis API с откатом на эвристику по именам.AnalysisApiTypeResolver.kt
РаскладкаELK 0.9.1 (layered и сплайны) как геометрический движок. Фолбэк — свой послойный алгоритм.MelElkLayout.kt, MelGraphLayout.kt
ХолстСвой Java2D: база MelCanvas отвечает за пан, зум и вписывание. Цвета — JBColor из единой палитры.MelCanvas.kt, MelGraphView.kt, MelPalette.kt
Панели-отчётыHTML в JEditorPane со ссылками в исходник: инспектор, «Проблемы».HtmlReportPane.kt и наследники
ОбвязкаToolWindowFactory, JBSplitter, JBList, SearchTextField, ComboBox, ActionToolbar, свой сегментный переключатель.MelToolWindowFactory.kt, CatalogPanel.kt, MelSegmentedControl.kt
Состояние UIЕдинственная точка истины: панели шлют намерения и слушают уведомления на EDT.MelSelectionController.kt
ПревьюВнутренний API рендера Android Studio: Layoutlib, модель как у встроенного Compose Preview. Методы, сменившие сигнатуру между версиями студии, вызываются рефлексией.MelComposeRenderer.kt, PreviewPanel.kt
ИнспекцииLocalInspectionTool и общий PSI source-root с detekt-правилами.inspection/, tools/detekt-rules/src/psi/kotlin
Почему так
  • Нет JCEF. В JBR целевой студии нет нативного CEF, поэтому браузерный рендер (Mermaid.js, D3) отпал, и граф рисуется своим Java2D.
  • Исходник вместо jar. Jar mel-graph собран против kotlin-compiler-embeddable, которого в IDE нет. Поэтому в плагин компилируется исходник ядра; тем же приёмом подключены PSI-хелперы detekt-правил.
  • ELK без Guava. ELK забандлен в плагин, но Guava из его зависимостей исключена и берётся из платформы. Вторая копия давала LinkageError при рендере превью.

Тесты:

  • :tools:mel-graph:test — экстрактор на эвристическом резолвере (включая формы переходов боевых экранов, TransitionShapesTest), линтер, производные, карта экранов, эмиттеры, классификация источников подписок;
  • :tools:mel-idea-inspections-test — инспекции на лёгкой платформе.

IDE-резолвера на Analysis API и самого тул-окна тесты не касаются.

Источник истины по поведению — код в :tools:mel-idea-plugin и :tools:mel-graph репозитория sbbol_android_mel, по архитектуре MEL — :core:mel, затем спека. Если документ разойдётся с кодом, прав код.