Пользователь
0,0
рейтинг
19 июля 2013 в 13:06

Разработка → Разработка плагина IntelliJ IDEA. Часть 1 перевод

За последнее время у меня накопилось достаточно материалов по разработке плагинов для IntelliJ IDEA, чем и собираюсь поделиться с хабрасообществом.

Среда разработки и инфраструктура


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

Для разработки плагинов подойдет любая современная версия Intellij IDEA – она уже включает в себя полный набор необходимого инструментария.
Для настройки среды разработки следует выполнить несколько шагов:
  • скачать исходные коды соответствующей версии Intellij IDEA Community Edition;
  • создать SDK типа «Intellij Platform Plugin SDK» (на рисунке ниже) и указать путь до установленной Community Edition (можно использовать Ultimate Edition, но отладка функций внутреннего API работает только в CE);
  • в настройках SDK, на странице Sourcepath необходимо указать путь до загруженных на п.1 исходных кодов;
  • создать новый модуль с типом «Platform Plugin» и присвоить ему ранее созданный SDK.



После чего можно приступать к разработке плагина как обычного Java-проекта, но с дополнительной возможностью видеть исходный код внутреннего API и трассировать его исполнение в отладчике.

Номера сборок


Это ограничения на диапазон поддерживаемых версий, используемые интегрированной средой для определения возможности корректной работы конкретного плагина. Начальный и конечный номера билдов указываются в файле plugin.xml, как и прочая метаинформация.
Начиная с IntelliJ IDEA 9 используется составная нумерация билда: например, IU-90.94. Этот номер состоит из следующих частей:
  • идентификатор продукта (IC для IDEA Community, IU для IDEA Ultimate, RM для RubyMine и PY для PyCharm);
  • номер ветви;
  • номер билда в этой ветке.

Каждый раз, когда создается очередная релизная ветвь одного из продуктов, основанных на платформе IntelliJ, номер ветки увеличивается на 1, а номер в транке на 2, таким образом, нестабильные билды имеют четный номер, а стабильные – нечетный.

Составные номера билдов могут применяться в тегах since-build и until-build в файле plugin.xml. Обычно идентификатор продукта опускают, используя лишь номер ветки и билда: <idea-version since-build="94.539"/>

Совместимость плагинов с продуктами на платформе IntelliJ


Все продукты, базирующиеся на IntelliJ Platform (IntelliJ IDEA, RubyMine, WebStorm, PhpStorm, PyCharm и AppCode) разделяют общий нижележащий API платформы. Таким образом, плагины, не использующие какую-либо специфичную Java функциональность могут быть помечены как совместимые с другими продуктами, а не только с самой IDEA. Сделать это можно определив зависимости от модулей в файле plugin.xml.
Зависимости помещаются внутри тега
, содержимое тега начинается с com.intellij.modules. Например:
<idea-plugin version="2"> ... <depends>com.intellij.modules.lang</depends> ... </idea-plugin>

Если плагин не включает теги с зависимостями в plugin.xml, то он считается устаревшим и способен запуститься только в IDEA. Если plugin.xml содержит по крайней мере один такой тег, то он загрузится если в продукте содержатся все модули на которые он ссылается.
На данный момент следующие модули доступны во всех продуктах семейства IntelliJ:
  • com.intellij.modules.platform;
  • com.intellij.modules.lang;
  • com.intellij.modules.vcs;
  • com.intellij.modules.xml;
  • com.intellij.modules.xdebugger.

А эти модули доступны только в соответствующих продуктах:
  • com.intellij.modules.java – IntelliJ IDEA;
  • com.intellij.modules.ruby – RubyMine;
  • com.intellij.modules.python – PyCharm;
  • com.intellij.modules.objc – AppCode.

PhpStorm не обладает специфичным модулем, но включает в себя PHP плагин, который тоже возможно использовать как зависимость: com.jetbrains.php.
Также возможно использовать необязательные зависимости. Если плагин работает со всеми продуктами, но предоставляет некоторую специфичную Java функциональность, то можно использовать следующий тег:

<depends optional="true" 
         config-file="my-java-features.xml">com.intellij.modules.java</depends>

Перед определением плагина совместимым с остальными продуктами, стоит убедиться, что не используются никакие функции специфичные для API IntelliJ IDEA. Для того чтобы сделать это, необходимо создать SDK указывающей на установленный RubyMine или PyCharm, скомпилировать плагин с этим SDK и проверить его работоспособность.

Структура плагина IntelliJ IDEA


Плагины – это единственный поддерживаемый путь расширения функциональности IDEA. Плагин использует предоставляемый программный интерфейс среды или других плагинов для реализации собственной функциональных возможностей. Обратим внимание на структуру и жизненный цикл плагина.

Содержимое плагинов

Существуют три способа организации содержимого плагина.
Первый – плагин содержит один jar-файл, размещенный в папке plugins. В архиве должен находиться конфигурационный файл (META-INF/plugin.xml) и классы, которые реализуют функциональность плагина. Конфигурационный файл определяет имя плагина, описание, данные о разработчике, поддерживаемая версия IDE, компоненты, действия, группы действий.
.IntelliJIDEAx0
    plugins
        sample.jar/
            com/foo/.....
                ...
                ...
            META-INF
                plugin.xml

Второй способ – файлы плагина размещены в папке:
.IntelliJIDEAx0
    plugins
        Sample
            lib
                libfoo.jar
                libbar.jar
            classes
                com/foo/.....
                ...
                ...
            META-INF
                plugin.xml

Classes и lib автоматически добавляются в classpath.
Третий способ – файлы плагина помещаются в jar-файл, находящийся в папке lib:
.IntelliJIDEAx0
    plugins
        Sample
            lib
                libfoo.jar
                libbar.jar
                Sample.jar/
                    com/foo/.....
                    ...
                    ...
                    META-INF
                        plugin.xml


Загрузчики классов

Чтобы загрузить классы каждого плагина, IDEA использует раздельные загрузчики классов. Это позволяет использовать различные версии библиотеки, даже если она используются самой IDEA или другим плагином.
По-умолчанию, основной загрузчик классов загружает только те классы, которые не были найдены загрузчиком плагина. Тем не менее, в plugin.xml, в секции depends можно определить зависимости от других плагинов. В таком случае, загрузчики классов этих плагинов будут использованы для разрешения не найденных классов в текущем плагине. Это позволяет ссылаться на классы из других плагинов.

Компоненты плагинов

Компоненты – это фундаментальный концепт интеграции плагинов. Существуют три вида компонентов:
  1. уровня приложения;
  2. уровня проекта;
  3. уровня модуля.

Компоненты уровня приложения создаются и инициализируются во время старта IntelliJ IDEA.
Они могут быть получены от экземпляра класса Application с помощью метода getComponent(Class).

Компоненты уровня проекта создаются для каждого экземпляра класса Project (они могут быть созданы даже для неоткрытого проекта). Их можно получить от экземпляра Project вызовом метода getComponent(Class).

Компоненты уровня модуля создаются для каждого модуля в каждом проекте, загруженном в IDEA. Они могут быть получены аналогичным методом от экземпляра Module.

Каждый компонент должен реализовать интерфейс, определенный в конфигурационном файле. Интерфейс класса будет использован для получения компонента из других компонентов, реализация класса будет использована в процессе инициализации компонента. Важно отметить, что два компонента на одном уровне (модуля, проекта, приложения) не могут иметь одинаковый интерфейс.

Каждый компонент обязан иметь уникальное имя, которое будет использовано при экспортировании и прочих внутренних нуждах. Имя компонента возвращает метод getComponentName(). Рекомендуется применять следующее имя компонента: <имя плагина><точка><имя компонента>.

Дополнительно, класс, реализующий интерфейс компонента уровня приложения также может реализовывать интерфейс ApplicationComponent. Компонент приложения, не имеющий ни одной зависимости должен предоставлять конструктор без параметров, который будет вызван при создании. Если компонент зависит от других, он может принимать их как параметры конструктора, IDEA гарантирует, что эти компоненты будут корректно инициализированы в правильном порядке.

Компоненты уровня приложения должны быть объявлены в секции <application-components> файла plugin.xml.
IntelliJ IDEA предлагает упрощенный путь создания компонентов приложения, со всей необходимой инфраструктурой. В интерфейсе IDEA предусмотрены инструменты для определения реализации классов компонентов и автоматического изменения секции <application-component> файла plugin.xml. Для того чтобы создать и зарегистрировать компонент приложения следует выполнить следующие шаги:
  1. выбрать New в контекстном меню Java пакета или нажать сочетание Alt+Insert;
  2. в подменю New выбрать Application Component;
  3. откроется диалог New Application Component, в котором необходимо ввести название компонента и нажать OK.

IDEA сгенерирует новый класс в выбранном пакете, реализующий интерфейс ApplicationComponent, зарегистрирует его в plugin.xml, добавит новый узел в Module Tree View и откроет класс в редакторе.

Классы, имеющие функциональность компонента уровня проекта реализуют интерфейс ProjectComponent. В конструкторе класса компонента должен быть параметр типа Project, если он использует экземпляр проекта. Также он может принимать другие компоненты уровня приложения или проекта как параметры, при наличии зависимости от них.

Компоненты уровня проекта должны быть зарегистрированы в секции <project-components> конфигурационного файла. Как и в случае компонентов приложения можно воспользоваться помощью IDE, выбрав подменю "New | Project Component".

Компоненты уровня модуля реализуют интерфейс ModuleComponent. Зависимости компонента могут быть переданы как параметры конструктора. Компоненты должны быть зарегистрированы в <module-components> вручную или посредством выполнения пункта контекстного меню "New | Module Component".

Сохранение состояния компонентов

Состояние каждого компонента будет автоматически сохраняться и загружаться, если класс реализует один из интерфейсов: JDOMExternalizable (устарел, не рекомендуется к применению) или PersistentStateComponent.
Если класс компонента реализует интерфейс PersistentStateComponent, то состояние сохраняется в XML файле, который указан в аннотации @State или @Storage (по-умолчанию, помещается в файл <имя компонента>.xml).
Если класс реализует JDOMExternalizable, то состояние сохраняется в следующих файлах:
  • компоненты уровня проекта сохраняют состояние в файле проекта (.ipr), либо при включенной в plugin.xml опции «workspace» – в файле .iws;
  • компоненты уровня модуля сохраняют данные в .iml файл.

Жизненный цикл компонентов

Компоненты загружаются в следующем порядке:
  • создание – выполнение конструктора;
  • инициализация – вызов метода initComponent (если реализован интерфейс ApplicationComponent);
  • конфигурация – вызов readExternal (если реализован JDOMExternalizable) или loadState (если реализован PersistentStateComponent и компонент в состоянии не по-умолчанию);
  • для компонентов уровня модуля – вызов moduleAdded (если реализован ModuleComponent);
  • для компонентов уровня проекта – projectOpened (если реализован интерфейс ProjectComponent).

Компоненты выгружаются в следующем порядке:
  • сохранение конфигурации – вызов writeExternal (если реализован интерфейс JDOMExternalizable) или getState (если реализован PersistentStateComponent);
  • освобождение ресурсов – вызов метода disposeComponent.

В конструкторе компонента запрещено использовать метод getComponent() для получения каких-либо зависимостей. Если данному компоненту нужно получить другие компоненты при инициализации, они должны быть переданы в параметрах конструктора, либо перенести инициализацию в метод initComponent().

Расширения плагинов и точки расширения


Intellij IDEA предоставляет концепт расширений и точек расширения, которые позволяют взаимодействовать с другими плагинами и ядром IDEA.
Если требуется, чтобы плагин позволял расширять его функциональность, то необходимо определить одну или несколько точек расширения. Каждая такая точка определяет класс или интерфейс, который определяет протокол доступа к ней. Это же касается и расширений плагина.

Определить расширения и точки расширения можно в конфигурационном файле в секциях
 и  соответственно.
Чтобы зарегистрировать точку расширения нужно добавить дочерний элемент внутри секции , который содержит название точки, класс или интерфейс, расширяющий функциональность плагина. Для примера рассмотрим фрагмент файла plugin.xml:
<extensionPoints> <extensionPoint name="MyExtensionPoint1" beanClass="MyPlugin.MyBeanClass1"> <extensionPoint name="MyExtensionPoint2 interface="MyPlugin.MyInterface"> </extensionPoints>

Атрибут "interface" устанавливает интерфейс, который должен быть реализован для расширения функциональности. Атрибут "beanClass" определяет класс, содержащий одно или несколько свойств помеченных аннотацией @Attribute. Плагин, предоставляющий точку расширения прочитает эти свойства из файла plugin.xml.
Рассмотрим пример с MyBeanClass1:
public class MyBeanClass1 extends AbstractExtensionPointBean {
  @Attribute("key") public String key;
  @Attribute("implementationClass") public String implementationClass;
 
  public String getKey() {
    return key;
  }
 
  public String getClass() {
    return implementationClass;
  }
}

Чтобы объявить расширение с доступом к точке расширения MyExtPoint, конфигурационный файл должен содержать тег с атрибутами "key" и "implementationClass" с соответствующими значениями.

Для регистрации расширения требуется выполнить следующие шаги:
  1. в элементе установить значение атрибута «xmlns» (устарел) или «defaultExtensionNs» одним из следующих:
    • "com.intellij", если плагин расширяет функциональность ядра IDEA;
    • <идентификатор плагина>, если плагин расширяет функциональность другого плагина.


    добавить новый дочерний элемент в секцию , название тега должно совпадать с идентификатором точки расширения;
    определить тип точки расширения, выбрав из следующих:
    • если точка расширения объявлена с атрибутом "interface", то в дочернем элементе необходимо указать атрибут "implementation", значение которого – класс, реализующий этот интерфейс;
    • если точка расширения объявлена с атрибутом "beanClass", то дочерний элемент должен содержать все атрибуты, которые были помечены в этом классе аннотацией @Attribute.



    Действия (Actions)


    Также Intellij IDEA предоставляет концепт действий (actions).
    Действие – это класс, наследуемый от AnAction, чей метод actionPerformed() вызывается, когда выбран элемент меню или кнопка тулбара.

    Действия объединяются в группы, которые также могут содержать вложенные группы. Группы действий могут быть отображены как меню или тулбары. Подгруппы отображаются как подменю.
    Позже действия будут рассмотрены более подробно.

    Сервисы


    Сервис – это компонент, загружаемый по требованию, когда плагин вызывает метод getService() класса ServiceManager. Intellij IDEA гарантирует, что будет создан только один экземпляр сервиса, независимо от того сколько раз был вызван метод.

    Сервисы должны иметь интерфейс, определенный в plugin.xml. Класс с реализацией будет использован при создании сервиса.

    Сервисы подразделяются по уровням подобно компонентам, т.е. на сервисы уровня приложения, проекта и модуля, которым соответствуют точки расширения applicationService, projectService и moduleService соответственно.

    Чтобы объявить сервис необходимо:
    • добавить соответствующий дочерний элемент (, , ) в секцию ;
      для добавленного элемента установить следующие атрибуты:
      • "serviceInterface" – интерфейс сервиса;
      • "serviceImplementaion" – реализация сервиса.



      Классы интерфейса и реализации могут совпадать.
      Пример из файла plugin.xml:
      
      <extensions defaultExtensionNs="com.intellij">
          <!-- сервис уровня приложения -->
            <applicationService serviceInterface="Mypackage.MyServiceInterfaceClass" serviceImplementation="Mypackage.MyServiceImplClass">         
            </applicationService>
       
          <!-- сервис уровня проекта -->
            <projectService serviceInterface="Mypackage.MyProjectServiceImplClass" serviceImplementation="Mypackage.MyProjectServiceImplClass">         
            </projectService>
       </extensions>
      

      В следующей части: конфигурационный файл, действия, проекты и др.

      Все статьи цикла: 1, 2, 3, 4, 5, 6, 7.
Перевод: JetBrains
Александр @Lucyfer
карма
32,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (13)

  • 0
    Спасибо, таких статей очень не хватает. Если есть возможность, напишите пож-ста про PSI-Model проекта и навигацию между компонентами.
    • 0
      Про PSI обязательно будет, это, как ни крути, одна из ключевых особенностей IDEA.
      Насчет навигации — не понятно между какими именно компонентами?
      • 0
        Я может не очень удачно выразился, я имел ввиду переход по Ctrl+Click к файлу, xml-конфигурации, определенному месту в коде и т.п. Спасибо.
      • +2
        Незнаю насколько это полезно в общем случае, но когда я начинал у меня были следующие вопросы по PSI:

        1) Что такое стабы и как они используются.
        2) Чем AST отличается от PSI, при том что часть AST нод также является и PSI нодами. В чём польза AST нод, когда их надо отделять от PSI элементов, а когда нет?
        3) Что такое лёгкие AST ноды и зачем они нужны?
        4) Ленивые AST ноды.
        5) Собственно, полный цикл от лексер->IElementType->парсер->AST дерево или лёгкое AST дерево->стаб или PSI. Зачем делают несколько лексеров, обычный и highlighting, в чём их разница? Почему некоторые языки объявляют парсер в ParserDefinition, а некоторые (типа Java) — делают это как-то совсем по-другому, и.т.д, аналогично про создание PSI по ASTNode, когда-то через ParserDefinition, когда-то через createPsi.

        Начиная с момента когда PSI уже построен всё было понятно более-менее сразу, там, конечно, порой тоже чёрт ногу сломит, но по крайней мере концепции ясны.

        P.S. Возможно половина вопросов от того, что я сразу на реализацию Java смотрел.
        • 0
          2) AST конкретное языконезависимое синтаксическое дерево, PSI языкозависимый набор интерфейсов сверху. Разделение появилось в процессе развития. У новых плагинов поверх Open API разделение AST / PSI происходит автоматом.
          1) Стабы — абстрактное (существенное компактное) дерево, которое хранится на диске и может использоваться PSI для определенных (но не всех) операций. Если используется неподдерживаемая операция, то стабы замещаются AST. Например, для файлов, загруженных в редактор, используется AST.
          • 0
            2) А вот реализация Java реализует свои ASTNode, в частности, с целью поиска детей (findChildByRole, например). Это теперь неправильно, что-ли? Получается, что такие ASTNode не совсем языконезависимы, т.к они специфичны для конкретного языка. Я вообще так и думал поначалу, что AST строится само собой, грубо, говоря по типу элементов (композитный-не композитный), пока на Java не посмотрел. Там как-то сложно всё. В Java не-стабовые PSI зачастую сами же AST нодами и являются.

            Плагины поверх Open API — это которые, например?
            • 0
              смешивать AST / PSI можно, но это делает код сложнее, чем надо. Например, для java пришлось делать light psi elements, которые уже отвязаны от дерева.
              Groovy, Scala, Ruby, JavaScript, Python, PHP, etc уже работают в разделении AST и PSI. Дерево строится унифицированно посредством PsiBuilder, а PSI лениво создается по дереву
        • 0
          3. Light элементы — это теже элементы синтаксического дерева, ток его можно создать не через Лексику, а вручную. Зачастую ипользуется когда нужно возратить PSI елемент, который не является частью дерева.

          4. Ленивые ноды, ноды которые парсятся при первом вызове. Это теже PsiFile работает через Lazy. Лейзи ноды используются например, ещё для Иньекций в основоное PSI дерево, другое PSI дерево другого языка(например JavaDoc парсинг в комментах явы)

          5. В яве это сделано ибо — там есть поддержка уровней языка. Для каждого уровня — лексика немного отличается(кейворды), да и парсинг тоже.

          Java имеет очень специфичную AST + PSI реализация
          • 0
            Если точно помню — XML плагин имеет подобную реализацию
  • 0
    Отлично! А вот подскажите, как вот такое сделать в виде плагина? youtrack.jetbrains.com/issue/IDEA-80019
    Сам пытался разобраться в прошлом году, неудачно. Может, подскажете, в какую сторону копать? А с меня статья :)
    • 0
      К сожалению не сталкивался с VCS-плагинами. Возможно стоит посмотреть как это реализовано в git или mercurial плагинах (исходный код есть на гитхабе).
  • 0
    Большое спасибо за статью!
    К сожалению, очень не хватает информации по разработке плагинов к идее. Никакой полноценной информации по API идеи нет, а лазить в чужом коде и пытаться понять, что там к чему, очень трудно.
    • 0
      Согласен. Сам достаточно много времени провел за чтением JavaDoc и исходников.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.