В этом посте я описываю техническую настройку небольшого языкового эксперимента, который я разработал, чтобы помочь себе в работе над пользовательскими инструментами Xcode.
Необходимость более гибкого кодирования внутри Xcode
В прошлом месяце я создал прототип Swift IDE и очень заинтересовался в улучшении практики разработки на Swift. (т.к. Apple не нанимает удаленных специалистов, мне интересно, какие еще компании работают в этой области).
Чуть позже я вернулся к обновлению моего пользовательского инструмента Xcode под названием Timelane для поддержки отладки нового кода на основе новых современных API параллелизма Swift:
![](https://habrastorage.org/getpro/habr/upload_files/33a/58d/6fa/33a58d6fac0517d4ec3f445ca47b98ea.webp)
В отличие от предыдущих версий Timelane, которые поддерживали Combine и RxSwift, основанные на очень минимальных протоколах, новый инструмент должен был поддерживать массу различных API, таких как TaskGroup(), TaskGroup.addTask(), Task(), Task.detached() и другие.
Простые пользовательские инструменты Xcode определены в одном файле XML, и как только я достиг отметки в 1000 строк, разработка захромала. Я попробовал несколько креативных подходов, таких как включение ASCII-графики в меню перехода Xcode:
![](https://habrastorage.org/getpro/habr/upload_files/3e1/7f8/556/3e17f8556cd86954cd359b599a2d3273.webp)
Но в конечном итоге тысячи строк плоского XML просто неуправляемы во время активной разработки. Поэтому, я сделал глубокий вдох и пошел в обход, чтобы создать простое языковое расширение, которое подключается прямо к Xcode…
Предварительные и последующие действия в Xcode
У меня есть минимальный опыт работы с React и Vue.js, и я являюсь фанатом идеи транспиляции: разработки крутых/мощных API, которые «разворачиваются» в менее элегантный/удобный код на целевом языке! Это беспроигрышный вариант.
Поэтому, я изучил возможные способы интеграции чего-либо быстрого в Xcode. Вы можете добавить сценарии для автоматического запуска таких событий, как сборка, запуск, тестирование и т. д., и я обнаружил, что мне нужны ровно две вещи, которые Xcode предлагает из коробки:
«Пред-действие сборки» — для анализа и преобразования моего пользовательского кода в целевой формат Xcode XML.
«Выполнение пост-действия» — очистить исходный код от любого сгенерированного транспилированного кода.
![](https://habrastorage.org/getpro/habr/upload_files/cf7/a47/44b/cf7a4744b0cb0b153073cd3c9bff3f04.webp)
Таким образом, редактируя код, я могу работать с чистой, минимальной версией кода, используя свой собственный синтаксис.
Как только пакет изменений сформирован, я делаю сборку, которая генерирует выходной XML, заставляющий Xcode работать над его проверкой и отображением ошибок и предупреждений.
Как только ошибки будут устранены, я могу запустить проект для тестирования пользовательского инструмента в Instruments, обращаясь при этом к полному сгенерированнсму коду. Когда я выхожу из пользовательского инструмента, я снова могу работать с минимальным пользовательским кодом.
Это чистый, простой процесс, который помог мне легко работать над инструментом, который в настоящее время содержит более 2000 строк XML в «распакованном» виде.
Включения XML
Прежде всего, я хотел разбить монолит, извлекая различные части в четкую иерархию папок и файлов. Я добавил команду include, которая вносит файл XML во время сборки, а затем удаляет внесенный код во время очистки:
![](https://habrastorage.org/getpro/habr/upload_files/e5c/44c/77e/e5c44c77eaa0b3f35c0b3a899f20f2e7.webp)
Разделение тысяч строк плоского XML на ограниченные файлы меньшего размера, содержащие до пары сотен строк, упростило навигацию по кодовой базе.
Я решил использовать команды, заключенные в XML-комментарии, чтобы сделать мой пользовательский код действительным XML — таким образом, пользовательский синтаксис не вызывает ошибок в редакторе кода Xcode.
На этапе сборки теги выглядят следующим образом:
<!-- include "tasks-instrument.xml" -->
И вставляет содержимое целевого файла после расположения исходного тега следующим образом:
<!-- include "tasks-instrument.xml" -->
<!-- included "tasks-instrument.xml" "982F43E4-59E0-45A4-A199-18C3DED2730E" -->
<!-- MARK: - TASKS INSTRUMENT -->
<!--
The tasks instrument.
-->
<augmentation>
<id>${instrumentID}.byname</id>
<title>Show Tasks by Name</title>
<purpose>A view that provides insight into individual tasks on the timeline.</purpose>
...
<!-- / included "tasks-instrument.xml" "982F43E4-59E0-45A4-A199-18C3DED2730E" —>
После того, как я закончу отладку инструмента, на этапе очистки будет удален код между открывающим и закрывающим тегами «included». (Я не говорю, что этот процесс является сверх-надежным, но должен сказать, что для моего варианта использования он работает отлично.)
Когда включения работали в соответствии с моими потребностями, я мог разработать структуру глубиной в нескольких уровней, которая делала обоснованным и легким в работе каждый компонент в пакете. Ниже приведена часть фактической иерархии:
Instrument package
+- tasks-schema.xml
+- schema-metadata.xml
+- region-tracking-partial.xml
+- [Custom XML]
-- region-tracking-open-interval-partial.xml
+- region-tracking-open-interval-partial.xml
...
+- task-groups-schema.xml
+- schema-metadata.xm
l +- region-tracking-partial.xml
-- region-tracking-open-interval-partial.xml
Возможно, вы уже заметили это, но взгляните еще раз выше — обе схемы включают а) «schema-metadata.xml», b) «region-tracking-partial.xml» и c) «region-tracking-open-interval- partial.xml». Некоторые из этих включений переиспользуются слово в слово, и это резко уменьшает общий объем кода, который необходимо учитывать.
Другие включения не были идентичными, но различия были минимальными, поэтому (опять же, вдохновленный Vue.js) я решил добавить привязку данных к компонентам (в моем случае компоненты — это включения).
Привязки данных
Поскольку включения работают иерархически, также был смысл иметь иерархические привязки данных, например, подобно окружению в SwiftUI, дочерние элементы наследуют определения своих родителей и сливаются со своими собственными.
Во-первых, я добавил файл JSON, содержащий любые глобальные константы, не привязанные к конкретному компоненту:
![](https://habrastorage.org/getpro/habr/upload_files/f55/293/d5f/f55293d5f7a269f3c3bd6209ec902e87.webp)
И расширил включения для поддержки синтаксиса
<subsystem>"${instrumentID}.time"</subsystem>
<category>”DynamicStackTracing”</category>
Это позволило легко отрегулировать такие данные, как идентификатор инструмента, идентификаторы подсистем и id различных таблиц, а также версию инструмента и многое другое.
Что еще более интересно, я добавил синтаксис к тегам include для привязки данных — что действительно позволило им почувствовать себя полноценными компоненты. Например:
<os-signpost-point-schema>
<!-- include "schema-metadata.xml" id=“async-taskgroupresults-schema"
schemaTitle="Task Group Results" entity="taskgroups" -->
<!-- include "value-tracking-partial.xml" entity="TaskGroup" -->
</os-signpost-point-schema>
Все параметры, которые не являются распознанными свойствами тега, привязываются как данные к шаблону и его дочерним элементам.
Это позволило динамически создавать идентификаторы, имена и даже комментарии в сгенерированном коде:
<!-- The data table storing ${entity} data -->
<create-table>
<id>${entity}-table</id>
<schema-ref>async-${entity}-schema</schema-ref>
</create-table>
Что сделало весь код пакета максимально лаконичным, преобразовав более 2000 строк XML в менее чем 1000, разделенных на 20 файлов.
Условные выражения
Прежде чем подвести итог, я заметил, что могу повторно использовать еще больше кода, но некоторым инструментам это нужно, а некоторым нет. Мне пришлось добавить условные выражения (и, ей-богу, в этот момент я думал просто перейти на XSL, но я получал слишком много удовольствия от этого).
Чтобы реализовать условные включения, я добавил свойство тега where следующим образом:
<!-- include "tasks-augmentation.xml" where "${entity}==tasks" —>
Это позволило бы включать шаблон и связывать данные и т.д., только если условие where определялось как true.
Весь эксперимент с включениями занял всего несколько часов, но это позволило мне двигаться дальше и завершить сам инструмент, что в противном случае, вероятно, было бы невозможно.
Заключение
Создание пользовательских инструментов — это настолько нишевая работа, что мой эксперимент с включениями как таковой бесполезен в сравнении с моим конкретным сценарием использования (честно говоря, XML API должен был быть в первую очередь Swift DSL, если предполагалось, что разработчики действительно будут его использовать):
https://trycombine.com/images/timelane/powerups-demo.gif
В любом случае, я надеюсь, что эта статья была вдохновляющей — ваш повседневный рабочий процесс в Xcode может быть расширен множеством способов, и написание собственных языковых расширений или полноценного транспилятора возможно, пусть и не просто.
Честно говоря, мне жаль, что в прошлом я не искал возможности создания дополнительных языковых расширений вместо того, чтобы тратить кучу времени на рутинные задачи по написанию кода.
До следующего раза!
Куда теперь?
Если вы хотите поддержать меня, получите мою книгу о Swift Concurrency:
Хотите обсудить новый синтаксис async/await Swift и параллелизм? Свяжитесь со мной в твиттере по адресу https://twitter.com/icanzilb.