Существует укоренившееся мнение, что языковые расширения являются чем-то вроде фигур высшего пилотажа в программировании. Число публикаций на эту тему постоянно растет, однако доля русскоязычных среди них по понятным причинам ничтожна. Цель настоящего цикла статей — показать несложные и эффективные способы автоматизации обычных повседневных задач с помощью функционала для языковых расширений, доступного в средах разработки, основанных на JetBrains MPS.
В нашем случае такой средой будет Realaxy ActionScript Editor, бета-версию которого можно загрузить здесь. Впрочем, все изложенное ниже за несколькими несущественными частностями также применимо и для написания языковых расширений под Java в редакторе MPS.
С чего начать?
Самым простым и доступным применением потенциала платформы, специально заточенной под разработку предметных языков, будет создание в ней контекстного расширения на примере работы со строками. Наше пробное расширение будет решать задачу «умного» экранирования разных кавычек в случаях, подобных этому:
В строке встречаются и одинарные, и двойные кавычки. Наше учебное языковое расширение должно будет в зависимости от контекста предлагать экранирование символов, которые вызывают конфликт (экранирование двойных кавычек будет уместно только в тех строках, которые обрамлены одинарными, и наоборот). Такая демонстрационная задача хороша тем, что может решаться самыми разными способами, начиная от написания простеньких автоматизационных скриптов и заканчивая использованием плагинной архитектуры. В настоящей статье она будет решена с помощью Intentions, одного из аспектов языковых расширений в MPS.
Что такое Intentions?
Меню Intentions — одна из наиболее часто используемых функций любого редактора, основанного на IDEA. Это простой и быстрый контекстозависимый доступ к наиболее востребованным операциям, применимым к синтаксическим конструкциям языка (например, «Add Exeption», «Invert If Condition» или «Convert Variable to Field»). О доступности меню Intentions сигнализирует значок лампочки, появляющийся слева от строки.
В повседневной работе мы постоянно сталкиваемся с похожим функционалом, поскольку в том или ином виде он по умолчанию присутствует во всех современных IDE. В RASE (как, впрочем, и в MPS или в IntelliJ IDEA) список доступных для текущей позиции Intentions проще всего вызвать клавиатурным сочетанием Alt-Enter.
Более подробно об их сущности можно узнать из официальной документации на сайте JetBrains MPS.
Переходим от слов к делу
1. Сначала мы создаем новый проект. В функцию Main() помещаем две строки, одна будет обрамлена одинарными кавычками, а другая — двойными.
2. В редакторе имеется несколько режимов просмотра. ActionScript View, который предлагается нам по умолчанию, выглядит сравнительно аскетично и позволяет разработчику сфокусироваться непосредственно на AS. В MPS View, доставшемся редактору «по наследству» от одноименной платформы, раскрывается гораздо большее количество возможностей (в частности — для написания языковых расширений).
3. Далее в контекстном меню проекта (открывающемуся по правому щелчку мыши) добавляем новый язык:
4. Вводим имя языка (myLanguages.escapedStrings) и нажимаем на OK.
5. После указанных действий язык по имени myLanguages.escapedStrings успешно появляется новым рутом в нашем проекте. Можно заметить, что myLanguages.escapedStrings уже содержит некоторые так называемые аспекты (на скриншоте: structure, editor, constrains, typesystem и т.д). Позже мы подробно расскажем о том, как ими пользоваться.
6. Клавиатурным сокращением Alt-Enter или из контекстного меню языка myLanguages.escapedStrings вызываем диалоговое окно Language Properties.
7. В открывшемся окне во вкладке Dependencies в поле Extended languages выбираем com.realaxy.actionScript, после чего нажимаем ОК. Это означает, что это учебное языковое расширение мы будем создавать именно для ActionScript.
8. Добавим аспект Intentions. Intention — это некий автоматический скрипт, который выполняет определенное действие над выделенным языковым элементом (в нашем случае — экранированием кавычек в String Literal).
9. Создаем Intention declaration.
10. Присваиваем ей имя EscapeQuotes и привязываем к StringLiteral. После чего в блоке descriprion прописываем строку «Escape Quotes»
11. Наступает ключевой момент. На место конструкции isApplicable мы должны поместить условие, которому будет соответствовать наш Intention. Для этого не нужны особые ухищрения: мы работаем в среде с открытым исходным кодом, и нам достаточно лишь посмотреть, как в редакторе работает механизм работы подсветки ошибок и предупреждений. Наша цель — не изобретать велосипед, а использовать уже благополучно работающий код. Немножко отвлечемся и подумаем, где же его можно подглядеть? Для этого обратимся к ошибке, которую мы собираемся исправлять. Переведем курсор к нашей изобилующей кавычками строке, находящейся в Main(), чтобы вызвать контекстное меню.
12. Через контекстное меню или с помощью клавиатурной комбинации Shift-Ctrl-T (на Маке — Shift-Cmd-T) вызываем диалоговое окно Show Type. Оно по праву может считаться одним из самых полезных в AS-разработке, поскольку почти все системные сообщения (warnings, info и errors) так или иначе относятся к системе типов.
13. Итак, в появившемся окне мы видим информацию о том, что это за объект, что за тип и где он находится в иерархии типов. Нажимаем на кнопку Go To Rule Which Caused Error (как видно из предшествующего скриншота, для экономии времени мы могли бы сразу перейти к этой функции IDE). Переходим в появившуюся вкладку и видим конструкцию, из которой понимаем, что интересующий нас код скорее всего исполняется при вызове isCorrect. Только как до него добраться?
14. Если при нажатом Ctrl (или Cmd) навести указатель мыши на название метода, появится подчеркивание, напоминающее нам гипертекстовую ссылку. Кликнув на него, переходим к исходной точке кода. Она-то нам и нужна.
15. Далее копируем код метода isCorrect, переходим во вкладку IntentionDeclaration и вставляем его в метод isApplicable, чтобы упростить и модифицировать условие под наши нужды:
16. Теперь пришла пора добавить код, который будет выполняться при вызове нашего Intention. В методе execute прописываем модификацию нашего node.value
17. Стоп. Почему бы нам для большего удобства не воспользоваться регулярными выражениями? Сказано — сделано. По сочетанию Ctrl-L (Cmd-L на Маке) импортируем язык regexp.
18. В результате получаем следующую конструкцию...
19.… в которую остается лишь вставить соответствующий регексп.
Вся декларация Intention при этом выглядит следующим образом:
20. Наконец, Intention можно компилировать. Нажимаем Ctrl-F9 (на Маке — Cmd-F9) или в главном меню выбираем Build > Make Module(s), после чего переходим в Main(), пробуем запустить наше языковое расширение, нажав Alt-Enter на содержащей ошибку строчке… и замечаем, что ничего не происходит. В чем же дело?
21. Всё просто: мы скомпилировали языковое расширение, но не импортировали его в проект. По уже знакомому сочетанию Ctrl-L (Cmd-L на Маке) мы исправляем это недоразумение.
22. Снова нажимаем на нашей «неисправной» строке сочетание Alt-Enter.
23. И получаем искомый результат.
24. Что делать дальше — вопрос вкуса. Пока наше расширение работает только с строкам, заключенным в двойные кавычки. Поскольку мы хотим быстро сделать его применимым еще и к кавычкам одинарным, самый простой путь — элементарно продублировать с помощью Ctrl-D (Cmd-D) нашу декларацию Intention Escape Quotes и поменять в ней лишь тип данных, название и символ кавычек там, где это нужно.
25. Слегка адаптируем наш код:
Конечно, можно было добиться большей компактности кода и обойтись, например, одним рутом вместо двух на каждую из кавычек. Однако не забываем, что главной целью было получить быструю и эффективную автоматизацию рутинного действия, и в нашем случае эта задача решена просто и изящно.
26. Миссия выполнена. После повторной компиляции рута по Ctrl-F9 (на Маке — Cmd+F9) мы получаем готовый скрипт для экранирования кавычек. Вот результат его работы:
Вместо заключения
Как вы могли убедиться, автоматизировать выполнение повседневных задач в основанных на JetBrains MPS редакторах совсем несложно. Полученное решение легко перенести в другие проекты, добавив в их свойства наше языковое расширение.
Функционал, о котором рассказывает эта статья, является лишь первым шагом в мир LOP и DSL. Тем не менее, одного только языка Intentions с избытком достаточно, чтобы разработчик мог организовать «здесь и сейчас» собственную производственную инфраструктуру под свои задачи.
Тем же читателям, кто не собирается останавливаться на достигнутом и хотел бы более обстоятельно вникнуть в практику создания языковых расширений, советуем следить за обновлениями. В следующей статье та же самая задача будет решена другим способом, с более обширным использованием различных языковых аспектов.
Ссылка на исходники проекта.
Отдельное спасибо Евгению Потапенко (potapenko) за всестороннюю помощь и поддержку при написании этого материала.