Еще немного про разработку плагинов для IntelliJ

в 8:19, , рубрики: IDE, intellij idea, java, jetbrains, plugins, Песочница, метки: , , , ,

В последнее время на Хабре стали появляться статьи про создание расширений для Intellij IDE — одна, а вот и другая.

Я продолжу эту славную тенденцию и постараюсь описать те места Intellij OpenAPI, которых еще не коснулись; а примером будет plug-in с веселыми комиксами.

Еще немного про разработку плагинов для IntelliJ

Расширение, на самом деле, занимается всего одной простой вещью — отображает в панельке свежие картинки с моего любимого Geek&Poke, периодически выкачивая их с сайта и кэшируя на диск. Исходники, кстати, на GitHub'e.

Для особо бдительных — комиксы находятся под доброй лицензией CC BY-SA 3.0, так что все законно:)

Поскольку само создание проекта, написание plugin.xml и другие основные вещи — как и ссылки на соответствующую документацию — уже описаны в вышеупомянутых статьях, повторяться не будем; и я просто опишу несколько возникших у меня при разработке вопросов с их решениями.

Поддержка proxy

IntelliJ IDEA (да и другие IDE) умеют подключаться к сети через прокси, настройка подробно описана в документации.

А вот чтобы заставить свое расширение использовать глобальные настройки IDE, стоит посмотреть в сторону класса com.intellij.util.net.HttpConfigurable. В его публичных полях содержится вся необходимая информация: флаг USE_HTTP_PROXY, к примеру, говорит, используем ли мы вообще прокси или нет; а также есть информация о хосте, порте и пользователе.

Проще всего воспользоваться методом prepareURL, вызывая его для каждого соединения:

  /**
   * Call this function before every HTTP connection.
   * If system configured to use HTTP proxy, this function
   * checks all required parameters and ask password if
   * required.
   * @param url URL for HTTP connection
   * @throws IOException
   */
  public void prepareURL (String url) throws IOException {

Например, в коде для некоторого url это может выглядеть так:

    // Ensure that proxy (if any) is set up for this request.
    final HttpConfigurable httpConfigurable = HttpConfigurable.getInstance();
    httpConfigurable.prepareURL(url.toExternalForm());

На форуме JetBrains кто-то ругается, что этот метод не помогает — но, по-моему, зря они так.

Запуск процесса при инициализации plug-in'a

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

В этом поможет ApplicationComponent. Типы компонентов и их создание замечательно описаны в документации, в статье Plugin Structure.

Добавим в plugin.xml наш компонент:

    <application-components>
        <component>
            <implementation-class>com.abelsky.idea.geekandpoke.ComicsPlugin</implementation-class>
            <interface-class>com.abelsky.idea.geekandpoke.ComicsPlugin</interface-class>
        </component>
    </application-components>

А в нем самом определим метод initComponent:

public class ComicsPlugin implements ApplicationComponent {

    private static final int UPDATE_PERIOD = 15 * 60 * 60 * 1000;

    // Этот метод будет вызываться один раз при инициализации расширения;
    // если бы использовали ProjectComponent - то для каждого проекта.
    @Override
    public void initComponent() {
        startUpdateTimer();
    }

    private void startUpdateTimer() {
        final Timer timer = new Timer("Geek and Poke updater");
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                // А тут что-то делаем каждые 15 минут...
            }
        }, 0, ComicsPlugin.UPDATE_PERIOD);
    }

Локализация

Для локализации удобно использовать вот такой сниппет:

package com.abelsky.idea.geekandpoke.messages;

// ...

public class MessageBundle {

    private static Reference<ResourceBundle> bundleRef;

    // Сами тексты лежат в com/abelsky/idea/geekandpoke/messages/MessageBundle.properties
    //  - стандартный key-value .properties-файл.
    @NonNls
    private static final String BUNDLE = "com.abelsky.idea.geekandpoke.messages.MessageBundle";

    private MessageBundle() {
    }

    public static String message(@PropertyKey(resourceBundle = BUNDLE)String key, Object... params) {
        return CommonBundle.message(getBundle(), key, params);
    }

    private static ResourceBundle getBundle() {
        ResourceBundle bundle = null;
        if (MessageBundle.bundleRef != null) {
            bundle = MessageBundle.bundleRef.get();
        }

        if (bundle == null) {
            bundle = ResourceBundle.getBundle(BUNDLE);
            MessageBundle.bundleRef = new SoftReference<ResourceBundle>(bundle);
        }
        return bundle;
    }

}

Здесь стоит обратить внимание на несколько моментов.

Первое — храним ResourceBundle в SoftReference. Это достаточно распространенная практика в исходниках IDEA — держать как можно больше объектов в не-hard ссылках.

К слову, советую посмотреть на класс com.intellij.reference.SoftReference — именно его сами разработчики используют вместо реализации из java.lang.ref. Отличие в том, что при подозрениях в утечке памяти com.intellij.reference.SoftReference можно быстро переделать в hard-ссылку, а это поможет при профилировании.

Второе — аннотация org.jetbrains.annotations.PropertyKey. Она указывает на то, что аннотированный аргумент метода может являться только строкой из указанного в параметре resourceBundle бандла. Ее использование добавляет уверенности в том, что ключи в .properties-файле и в коде не рассинхронизировались (да еще и рефакторинг в IDEA многое учится делать, так как появляется связь между ключем и бандлом).

Третье — аннотации org.jetbrains.annotations.NonNls/org.jetbrains.annotations.Nls, помечающие строки, которые не должны (или, наоборот, должны) быть переведены. Документация от JetBrains по использованию — здесь.

Нотификации

При некоторых событиях хочется показать красивое уведомление. Такое, например:

Еще немного про разработку плагинов для IntelliJ

Здесь стоит смотреть в сторону класса com.intellij.notification.Notifications. Например, так:

    private void notifyNewEntry() {
        final Notification newEntryNotification = new Notification(
                /* Группа нотификаций */
                MessageBundle.message("notification.new.strip.group"),		

                /* Заголовок */
                MessageBundle.message("notification.new.strip.title"),

                /* Содержание */
                MessageBundle.message("notification.new.strip.content"),

                NotificationType.INFORMATION);

        // Не обязательно вызывать из UI-треда - внутри все равно будет сделан invokeLater.
        Notifications.Bus.notify(newEntryNotification);
    }

Группа уведомления отображается в настройках IDE, подробнее — в документации.

Еще немного про разработку плагинов для IntelliJ

Настройки

Еще немного про разработку плагинов для IntelliJ

Про то, как сделать страницу настроек для plug-in'a, подробно написано в документации.

Но если вкратце, то, во-первых, регистрируемся в plugin.xml:

    <extensions defaultExtensionNs="com.intellij">
    	<!-- ... -->
        <applicationConfigurable instance="com.abelsky.idea.geekandpoke.ui.SettingsPanel"/>
    </extensions>

Во-вторых, реализуем методы интерфейса com.intellij.openapi.options.Configurable. Из них самый важный — createComponent — должен вернуть компонент, на котором отображаются наши настройки.

Offline cache

Моему plug-in'у понадобилось хранить картинки на диске — в принципе, тот же вопрос встанет и при записи всяких кэшей, которым не место ни в директории проекта, ни в %TMP%. Можно, к примеру, записывать их куда-нибудь в %USERPROFILE%, а можно сделать интереснее — использовать для этого директорию, в которую сам plug-in установлен.

Расширения по-умолчанию устанавливаются в %USERPROFILE%/.IdeaIC11/config/plugins/PLUGIN_NAME; этот путь, впрочем, можно поменять установкой переменной idea.plugins.path в idea.properties.

    // PLUGIN_ID - значение элемента id в plugin.xml.
    final PluginId id = PluginId.getId(PLUGIN_ID);

    final IdeaPluginDescriptor plugin = PluginManager.getPlugin(id);

    // Путь к установленному расширению.
    File path = plugin.getPath();

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

P.S.

Надеюсь, этот короткий FAQ будет полезен начинающим копаться в платформе IntelliJ. А я, тем временем, приступаю к тому, ради чего и начал во всем этом разбираться — IDEA-плагину для поддержки одного очень интересного языка программирования;)

Автор: andy722

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js