Назад к блогу

От Magic Move до онбординга: как инструменты для разработчиков меняют подход к работе

От Magic Move до онбординга: как инструменты для разработчиков меняют подход к работе

Перевод рабочей машины на Linux почти всегда упирается в одно приложение, ради которого приходится держать вторую систему. Автор рассказывает, как столкнулся с этим при смене ОС, и описывает Bento — открытый офлайн-инструмент, который воспроизводит морфинг фигур и кода без тяжёлых парсеров. Особенно полезно тем, кто готовит технические доклады и хочет, чтобы анимация на слайдах не превращала код в кашу.

Почему Magic Move в Keynote оказался камнем преткновения при переходе на Linux

В 2025 году Rahul Ravikumar перевёл рабочий Framework 16 на CachyOS, отказавшись от двойной загрузки Ubuntu и Windows. Почти все инструменты нашли нативные сборки под Linux или получили достойные замены. Но одна жертва заставила притормозить: Apple Keynote и, в частности, эффект Magic Move. Для человека, который регулярно выступает на конференциях, презентационный инструмент — не украшение, а каркас рассказа. Именно на Keynote держались сложные технические концепции, которые требовалось показать легко.

Проблема в том, что смена ОС обходится дороже, чем кажется, если рабочий процесс завязан на проприетарное приложение. Остальные задачи решаются на Linux, но один инструмент остаётся на несовместимой платформе — и вы либо продолжаете держать для него вторую машину, либо миритесь с урезанной альтернативой. У Keynote есть и собственный изъян, который делает замену особенно болезненной: он не умеет анимировать фрагменты кода. На слайдах переходы между двумя версиями сниппета превращаются в кашу. val из первой строки улетает по диагонали и перетекает в al из calculate(), знаки препинания перемешиваются, как буквы в супе. Keynote оперирует глифами, а не кодом, поэтому чистая анимация возможна только через хрупкие обходные пути: разбить строки на невидимые боксы, вставить прозрачные символы-заглушки, вручную подогнать позиции токенов пиксель за пикселем.

Вокруг этого дефекта сложилась команда. Работая над Jetpack Compose — UI-тулкитом, который теперь поддерживает мультиплатформу, — инженеры каждую конференцию упираются в искорёженный переход и повторяют: «А что, если написать всю колоду слайдов на Compose, чтобы код анимировался правильно?» Некоторые действительно так и сделали, используя Shared Element transitions. Но сам Ravikumar не решился полностью отказаться от WYSIWYG-инструментов: для вёрстки, типографики и всего, что кодом не является, Keynote по-прежнему вне конкуренции. Оставалось найти способ перенести в другую среду именно ту функцию, без которой работа над выступлениями теряла смысл, — и подтолкнул к этому проект Bento, локальный офлайн-тулкит для презентаций, который уже умел морфинг фигур и формул, но пока не умел морфинг кода.

Как устроен Bento: локальный тулкит для анимации кода

Bento — открытый набор инструментов для презентаций, работающий полностью локально, без сети и облака. До недавнего времени он уже умел плавно перестраивать фигуры и математические выражения: позиция, цвета и градиенты интерполируются между слайдами. Не хватало одного — морфинга фрагментов кода.

Этот пробел закрыт в релизе 1.0.19. Теперь между двумя слайдами можно перейти так, чтобы val остался val, скобка осталась скобкой, а изменившийся литерал заменился аккуратно, а не рассыпался на отдельные символы. Реализация пришла из pull request, добавившего в Bento поддержку морфинга кода.

Посмотреть на код целиком можно в репозитории: github.com/tikurahul/warp. Если вы готовите доклады с техническими примерами и хотите, чтобы анимация не мешала рассказу, — Bento стоит попробовать хотя бы ради одного этого перехода.

Токенизация кода без AST: почему TextMate grammars подходят для фрагментов

Первая мысль при слове «токенизация» — построить AST. Разбираем исходник в дерево, получаем точные связи между узлами, диффим их. Но для анимации фрагментов кода это тупик по двум причинам.

Языков много, а сниппетов ещё больше. Поддержать AST-морфинг означает тащить в проект полноценные парсеры и компиляторную инфраструктуру под каждый язык. Это тяжело и хрупко.

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

Текстовые редакторы решают ту же задачу каждый день: им нужна быстрая и отказоустойчивая подсветка десятков языков. Один из инструментов — TextMate grammars. Формат придумали для редактора TextMate на macOS, позже его переняли Sublime Text, Visual Studio Code и Eclipse — то есть он стал отраслевым стандартом де-факто.

Грамматика — это список регулярных выражений по приоритету. Каждое находит свой токен и присваивает ему scope. Возьмём такой код:

val x = 10
print(x)

Грамматика разложит его в поток токенов:

Token(content='val', scopes='[source.kotlin, storage.type.kotlin]', lineNumber=0, startIndex=0, endIndex=3)
Token(content=' x ', scopes='[source.kotlin]', lineNumber=0, startIndex=3, endIndex=6)
Token(content='=', scopes='[source.kotlin, keyword.operator.assignment.kotlin]', lineNumber=0, startIndex=6, endIndex=7)
Token(content=' ', scopes='[source.kotlin]', lineNumber=0, startIndex=7, endIndex=8)
Token(content='10', scopes='[source.kotlin, constant.numeric.integer.kotlin]', lineNumber=0, startIndex=8, endIndex=10)
Token(content='print', scopes='[source.kotlin, support.function.kotlin]', lineNumber=1, startIndex=0, endIndex=5)

Первый scope всегда корневой — source.kotlin говорит, что перед нами исходник на Kotlin. Последний scope в списке описывает конкретную роль токена: storage.type.kotlin, constant.numeric.integer.kotlin. Именно по нему редактор выбирает цвет для подсветки, а вы — тип для анимации.

Дальше остаётся вопрос: какие признаки токена определяют его «личность» при диффе? Номер строки и смещения символов для этого не годятся — при рефакторинге токены как раз переезжают между строками, и в презентации это происходит постоянно. Структурную похожесть задают три вещи: content, основной scope (последний в списке) и depth — число вложенных scope. Глубина отделяет токен верхнего уровня от вложенного глубоко в тело функции:

class Token(
    /** Содержимое токена. */
    val content: String,
    /** Основной scope. */
    val scope: String,
    /** Глубина основного scope. */
    val depth: Int,
    /* Контекст для анимации (не влияет на структурную похожесть). */
    val lineNumber: Int,
    val startIndex: Int,
    val endIndex: Int
)

Такой набор признаков не требует ни компилятора, ни полного дерева — только грамматику и регулярные выражения. Полную реализацию можно посмотреть в репозитории автора: github.com/tikurahul/warp.

Diff-алгоритмы для анимации: от Myers к Heckel

git diff приучил нас доверять короткому патчу. Чем меньше правок — тем «правильнее» diff. Myers и Patience строят свою работу вокруг этой идеи: они ищут кратчайшую последовательность вставок и удалений, переводящую одну строку в другую. Для кода в репозитории это ровно то, что нужно.

Для анимации — нет. Посмотрите, что произойдёт при переименовании переменной: Myers может разрезать идентификатор пополам, удалить три символа слева, вставить два справа — только чтобы сэкономить один шаг редактирования. Формально счётчик стал меньше, а на экране токен разваливается на куски и прыгает по диагонали. Зритель видит хаос там, где вы хотели показать плавный переезд.

Heckel решает другую задачу. Его алгоритм (Paul Heckel, Communications of the ACM, 1978) работает за O(n) и не гонится за минимумом правок. Вместо этого он ищет якоря — токены, встречающиеся ровно один раз и в старом, и в новом фрагменте. Такой токен почти наверняка тот же самый до и после: он получает общую идентичность и просто скользит к новой позиции. Дальше идут проходы вперёд и назад от каждого якоря: соседние токены, совпадающие слева и справа, тоже спариваются. Что осталось несопоставленным в старом фрагменте — удалено, что осталось в новом — вставлено.

Разница в восприятии огромна. Myers оптимизирует число операций, Heckel — число сохранившихся связей. В анимации второе важнее: чем больше токенов доживают до конца как «те же самые», тем меньше мигающих исчезновений и тем цельнее выглядит переход. Минимальный патч может выиграть один шаг в статистике и проиграть всю сцену визуально.

Практическая реализация: якоря, прямые и обратные проходы в Heckel diff

Разберём механику на конкретном примере. Сравниваются две строки: val x = 10 и val x = 20. После токенизации обе разбиваются на пять токенов, и четыре из них совпадают по содержанию, области видимости и глубине: val, x , = и пробел. Меняется только последний — число 10 против 20.

Алгоритм ищет анкеры — токены, которые встречаются ровно один раз и в старом, и в новом потоке. Уникальность здесь заменяет нам гарантию идентичности: если токен val есть только один раз слева и только один раз справа, разумно считать, что это один и тот же токен до и после правки. В нашем примере анкерами становятся все четыре общих токена, а числовая константа — нет, потому что её представление изменилось.

Дальше начинаются проходы. Прямой проход идёт от каждого анкера вправо: алгоритм проверяет, совпадают ли токены на позициях previousIdx + 1 и currentIdx + 1. Пока совпадают — они связываются в пару, и указатели сдвигаются дальше. Как только токены расходятся, продвижение вперёд останавливается.

Обратный проход делает то же самое, но в противоположную сторону: проверяются позиции previousIdx - 1 и currentIdx - 1. Так попарно склеиваются токены, стоящие непосредственно перед анкером. Это важно, потому что вставка или удаление часто происходит в середине блока, а начало и конец остаются нетронутыми.

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

Покажем это на чуть более сложном случае. Было:

val x = 10
fun convert(input: Int) {
  // ...
}

Стало:

val x = 10
fun convert(input: Int) {
  println("The actual implementation")
}

Токены совпадают вплоть до тела блока. Комментарий слева — два токена // и ... — уходит в удаления. На их место встаёт вызов println(...), который распадается на семь вставленных токенов: само имя, открывающая скобка, кавычки, строковый литерал, закрывающие кавычка и скобка. А завершающая } снова оказывается анкером: она уникальна в обоих потоках, поэтому закрывающая скобка функции не прыгает, а спокойно остаётся на месте. Обратите внимание, что это не счастливая случайность — именно уникальность } делает её точкой привязки для обратного прохода.

Для анимации важен ещё один штрих. Если гасить удаляемые токены по одному, движение выглядит дёрганым. Поэтому идущие подряд удаления объединяются в группы и исчезают синхронно, а вставленные так же синхронно проявляются. Анкерные токены при этом плавно скользят к новым координатам. Полный код реализации доступен в репозитории warp.

Вклад в Bento: как сообщество развивает инструмент

Автор алгоритма и не собирался править Bento. Он лишь нашёл инструмент, которому не хватало морфинга кода, — и написал свою реализацию на стороне. Когда она заработала, оставалось перенести готовое решение в чужой репозиторий. Так появился pull request №259, вошедший в релиз 1.0.19.

Здесь стоит развернуть, что именно даёт такой вклад. Bento — полностью локальный презентационный инструмент с поддержкой морфинга фигур, позиций, цветов и градиентов. Не хватало одного: анимации переходов между фрагментами кода. Пул-реквест закрывает эту дыру и добавляет в проект новую возможность, а не правит опечатку в документации.

Как это выглядит со стороны контрибьютора:

  • Сначала — рабочий прототип в личном репозитории. Полная реализация доступна по адресу https://github.com/tikurahul/warp.
  • Затем — интеграция в Bento: код нужно согласовать с архитектурой проекта, его структурой данных и стилем.
  • В релиз 1.0.19 фича входит уже как часть официального инструмента.

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

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

Выводы и практический совет

Heckel diff решает задачу, для которой Myers не предназначен: он ищет не минимальное число правок, а устойчивые пары токенов, и потому даёт плавную анимацию, а не дёрганые перескоки. Регулярные выражения TextMate вместо полноценного парсера — осознанный компромисс: они переживают неполные сниппеты и не тянут за собой компилятор для каждого языка. Наконец, структурная идентичность токена определяется содержимым, основным scope и глубиной — номера строк и смещения символов в неё не входят, поскольку при рефакторинге токены как раз меняют строки.

Практический совет: если решите повторить этот путь в своём инструменте, начните с прототипа на стороне — отдельного репозитория или локальной ветки. Так вы сможете свободно экспериментировать с алгоритмом и порогом совпадения, не затрагивая чужой код, и лишь потом оформить готовое решение как пул-реквест в апстрим.

Похожее