В последнее время на Хабре стали появляться статьи про создание расширений для Intellij IDE — одна, а вот и другая.
Я продолжу эту славную тенденцию и постараюсь описать те места Intellij OpenAPI, которых еще не коснулись; а примером будет plug-in с веселыми комиксами.
Расширение, на самом деле, занимается всего одной простой вещью — отображает в панельке свежие картинки с моего любимого 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 по использованию — здесь.
Нотификации
При некоторых событиях хочется показать красивое уведомление. Такое, например:
Здесь стоит смотреть в сторону класса 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, подробнее — в документации.
Настройки
Про то, как сделать страницу настроек для 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