Дисклаймер: я не являюсь разработчиком из JetBrains, а жаль, и поэтому в статье и в коде могут быть и скорее всего есть неточности и ошибки. Часть 1 Введение Предусловия Создание основы языкового плагина Создание PSI-дерева - Лексер - Парсер Аннотаторы Подсветка скобок Часть 2 Форматирование Structure view Кэши, индексы, stub и goto Ссылки (Reference) Find Usages Rename и другие рефакторинги Маркеры (Markers)   Автодополнение Тестирование В предыдущей статье был рассмотрен процесс создания каркаса языкового плагина. В качестве примера использовались известные плагины для Java, go, Frege. Также приведены примеры из плагина для языка Monkey, который я разработал пока разбирался, как все работает. Так как у меня не было целью охватить всё, то плагин охватывает некоторое ограниченное подмножество языка. Сам интерпретатор можно найти тут. Документация, примеры в go-plugin, monkey. Все как всегда начинается с точки расширения. Класс должен имплементировать интерфейс  Наиболее важный - первый метод. Он возвращает модель форматирования, которая в свою очередить строится на блоках форматирования. Для упрощения можно воспользоваться методом FormattingModelProvider.createFormattingModelForPsiFile  Рассмотрим подробнее что такое блок форматирования. В IDEA блок форматирования представлен как интерфейс com.intellij.formatting.Block -  это некоторый диапазон текста (часто соответствующий какому-то PSI-элементу), к которому применяются правила форматирования. Блоки форматирования вкладываются друг в друга и образуют дерево.  Для того чтобы увидеть визуализацию этого дерева, можно воспользоваться инструментом PSI-Viewer (Tools->View PSI  Structure, раздел Block Sructure) Для упрощения реализации можно воспользоваться классом AbstractBlock. При этом вместо метода getSubBlocksон предлагает реализовывать метод buildChildren, который возращает список непосредственных дочерних элементов данного блока. getSpacing  - определяет количество пробелов или перенос строк между указанными дочерними элементами. Для упрощения реализации данной логики можно воспользоваться классом com.intellij.formatting.SpacingBuilder, который предоставляет удобное API для описания правил. Если не подходит builder, то можно создать свое описание через com.intellij.formatting.Spacing#createSpacing getIndent -  определяет правила отступа относительно родительского блока getWrap  - нужно ли переносить на другую строчку контент getAlignment - выравнивание блоков друг относительно друга getChildIndent - используется AbstractBlock для вычисления getChildAttributes , который возвращает правила для блока, создаваемого после нажатия клавиши Enter. В этом разделе пойдет речь об наполнении вот этой панели: За это отвечает точка расширения Класс должен имплементировать интерфейс: Для реализации StructureViewBuilder можно использовать уже готовые заготовки com.intellij.ide.structureView.TreeBasedStructureViewBuilder, com.intellij.ide.structureView.StructureViewModelBase и com.intellij.ide.structureView.impl.common.PsiTreeElementBase. Работа с PsiTreeElementBase  напоминает работу с блоками форматирования. Документация. Примеры реализации в plugin-go, Monkey.  Начнем с кэшей. IDEA опрашивает многие расширения по нескольку раз. Если расширение выполняет какую-то тяжелую работу, то самое лучшее решение - это закэшировать результат этой работы. IDEA предоставляет много готовых оберток для этого. Например, так вычисляется тип  Go выражения в плагине plugin-go: Здесь используется 2 менеджера. Первый - CachedValuesManager, который кэширует результат для psi-элемента, и второй - RecursionManager, который помогает предотвращать бесконечную рекурсию и StackOverflowError. Также есть специализированный кэш com.intellij.psi.impl.source.resolve.ResolveCache, который используется при разрешении (resolving) элементов (про это ниже). Документация. Все мы знаем как IDEA любит всё индексировать. Давайте посмотрим, что это такое и как это можно использовать.  Индексы в IDEA предоставляют возможность быстро находить файл с привязанной другой информацией или psi-элемент по выбранному ключу (например, по названию аннотации можно найти все места, где она используется). Посмотреть существующие индексы можно с помощью плагина Index viewer. О том, как все работает, также можно почитать в статье про Frege. IDEA поддерживает два вида индексов - File-based и Stub. File-based работает с содержимым файла, Stub работает с Stub-деревом, которое строится на базе Psi-дерева.  Пример использования можно посмотреть в плагине haskforce Начинается все как всегда с подключения точки расширения Файл должен расширять FileBasedIndexExtension. Получить результаты индексирования можно через  Плагин haskforce, например, использует этот тип индекса, чтобы получить все файлы в рамках модуля: Этот тип индексов, как мне кажется, используется чаще, так как позволяет искать по PSI-элементам (а точнее по stub, который отображает требуемую часть psi-дерева). Stub используются только для именнованных psi-элементов (которые имплементируют интерфейс PsiNamedElement). О них будет подробнее написано в разделе Reference Чтобы объявить новый индекс, используется следующая точка расширения: Класс должен имплементировать StubIndexExtension(или его наследников, например, StringStubIndexExtension).  Пример из плагина для Monkey: Примеры в go-plugin, frege. Теперь нам нужно научить IDEA создавать дерево Stub-ов и записывать необходимые элементы под нужным индексом.  Для каждого типа элементов, который мы хотим сохранить как Stub, мы создаем определение Stub. При этом корнем всех Stub-ов должен быть FileStub.  Пример из плагина для Monkey: Примеры из go-plugin (файл, элемент), Frege (файл, элемент) Следующий шаг - для каждого Stub создать описание типа элемента этого Stub. (Для автоматически генерируемых PSI-элементов с помощь Grammar-Kit плагина описания каждого типа элемента создается автоматически в соответствии с параметрами elementTypeHolderClass и elementTypeClass). ElementType для файла должен расширять IStubFileElementType,  для элемента - IStubElementType. IStubElementType требует реализации следующих методов: Как индексировать Stub указывается в методе indexStub . Например, в Monkey я использовал такую реализацию: Реализацию других методов можно посмотреть в примерах - плагин для Monkey, go-plugin, Frege Теперь наши stub-ы надо подключить к парсеру. Это делается в 2 шага. 1 шаг: определить свою фабрику типов элементов, для которых мы сделали IStubElementType, например как и указать в bnf файле, что теперь ее надо использовать для некоторых PSI-элементов: 2 шаг. Указать, что эти PSI-элементы должны расширять StubBasedPsiElementBase  Рассмотрим все вместе. В процессе индексации создается StubBasedPsiElementBase , затем с помощью IStubElementType.createStub создается Stub, который сериализуется (serialize) и ссылка на него сохраняется в индекс(indexStub).  Клиентский код, который работает со Stub, полученными из индексов, должен вызывать только те методы, для выполнения которых достаточно сохраненной информации. Поэтому в stub надо включать всю информацию, которая может понадобится в дальнейшем при анализе. Чтобы получить PSI-элемент, можно вызвать метод getNode(), но он дорогой, так как требует парсинга файла. Пример сохранения информации можно посмотреть в com.intellij.psi.impl.java.stubs.impl.PsiAnnotationStubImpl#getPsiElement , в котором используется текст из ASTNode. Индексы широко используются для go-to функций. Например, для работы данной панели: plugin.xml: MonkeySymbolContributor:  Многие плагины также используют индексы. Например, Request mapper (на данный момент не поддерживается, так как такая же функциональность появилась в IDEA), который помогает искать точки объявления REST-методов Под капотом RequestMapper использует такой код: Документация На данный момент идет изменение API в этой части, поэтому могут быть неточности. Ссылки создают связи между элементами, так, нажав Ctrl+B, вы перейдете на тот элемент, на который эта ссылка ссылается. Нажав Ctrl+B еще раз, вы увидите все элементы, которые ссылаются на этот элемент.  Элемент, который ссылается, и элемент, на который ссылаются, также могут иметь имя. Только тот элемент, который определяет это имя, должен реализовывать PsiNamedElement (а лучше PsiNameIdentifierOwner, их использование можно посмотреть в разделе Rename) Для того чтобы PSI-элемент мог предоставить ссылку, нужно реализовать методы Для упрощения реализации интерфейса PsiReference можно воспользоваться заготовкой PsiReferenceBase. Остается реализовать метод PsiElement resolve() илиResolveResult [] multiResolve(boolean incompleteCode) , которые возвращают элементы, на которые ссылаются. При реализации этого метода имеет смысл использовать специализированный кэш: После реализации данной части IDEA уже сможет предоставлять навигацию к элементу, на который ссылаются и обратно. Документация. Примеры go-plugin, monkey. Предоставляет возможность из этого меню: Точка расширения: Класс должен реализовать интерфейс FindUsagesProvider. Документация Одним из самых популярных рефакторингов является переименование (Shift+F6) За это отвечают две точки расширения: renameInputValidator должен реализовывать интерфейс RenameInputValidator  refactoringSupport должен расширять абстрактный класс RefactoringSupportProvider. Данный класс также содержит методы для поддержки других видов рефакторингов. На данный момент нас интересует метод, который указывает, поддерживается ли редактирование на месте. Теперь требуется реализовать методы переименования. Как уже было сказано до этого, PSI-элемент, который определяет имя, следует реализовывать интерфейс  PsiNamedElement. Нас здесь интересует метод setName. Одним из самых простых способов реализовать этот метод - это создать новый PSI-элемент из текста, например так И заменить элемент  Осталось реализовать переименование элементов, которые ссылаются на наш именованный PSI-элемент. Для этого нужно реализовать метод из PsiReference или воспользоваться готовой реализацией из com.intellij.psi.PsiReferenceBase, если она подходит.  IDEA активно использует подсказки в виде маркеров Примеры go-plugin, Frege, monkey Точка расширения: Файл должен реализовывать интерфейс LineMarkerProvider, методы которого возвращают объект LineMarkerInfo . Обратите внимание, что маркеры следует привязывать только к листьям PSI-дерева Думаю, все кто работает с IDEA любит, как она работает с подсказками (хотя, конечно, есть случаи, когда она немного тупит). Написать хороший механизм автодополнения очень сложно. Был даже цикл статье про это. Но при этом можно относительно быстро реализовать некоторые подсказки, например, вот такие: Это делается через реализацию метода PsiReference.getVariants, который должен возвращать все видимые подходящие значения. Фильтрацию по символам выполняет сама IDEA. Для более сложных кейсов можно воспользоваться точкой расширения: Класс должен реализовывать абстрактный класс CompletionContributor.  Документация. Примеры реализации go-plugin, frege, monkey (начальная поддержка автодополнения ключевых слов) Документация. Тестирование frege В IDEA тесты часто представляют собой несколько файлов с исходным кодом языка,  которые показывают состояние ДО и ПОСЛЕ действия. Для тестов обычно используется класс BasePlatformTestCase , а сами тесты выглядят как-то так: где RenameTestData.monkey и RenameTestDataAfter.monkey - файлы с исходным кодом до и после переименования. На этом рассказ о создании языкового плагина для IDEA закончен. Удачи в настройке своего IDE под себя!