Как написать плагин для программы

Функциональность программ
зачастую могут улучшать не только
их непосредственные разработчики,
но и другие люди. Они, как правило,
являются пользователями тех
программ, которые хотят улучшить.
Возможно это в тех случаях, когда
программа поддерживает различные
дополнения. Дополнения эти обычно
реализуются в виде плагинов.


Что такое плагины?

Конечно, возможны всякие
вариации, но обычно под плагином
понимается какая-то динамически
компонуемая библиотека (DLL — Dynamic Link
Library) специального формата, которая
благодаря находящимся в ней
функциям расширяет возможности
«родительского» приложения.
Конечно, в широком смысле слова под
плагинами можно понимать не только
DLL’ки, а, например, и такие
комплексные вещи, как дополнения к
Mozilla Firefox. Но обычно плагин — это
именно специальная динамическая
библиотека.

Чем же эта динамическая
библиотека такая вся из себя
специальная? Всё дело в том, какие
функции она экспортирует. Чтобы
приложение могло загрузить эту
библиотеку и использовать
содержащиеся в ней функции, ему
должен быть известен их вид — в
общем-то, это всегда так с
динамической компоновкой. Поэтому
все плагины для одной программы
имеют одинаковые имена и форматы
функций, используемых в приложении.
Конечно, среди экспортируемых
функций могут быть и другие, но
тогда спрашивается, зачем они там
нужны.

Универсального формата плагинов,
который подходил бы всем
приложениям, не существует. Причина
этого проста: очень разные функции
выполняют разные программы, и было
бы странно пользоваться плагинами
к Adobe Photoshop из Sound Forge. Но, тем не
менее, свои стандарты есть, а потому
приложения, выполняющие сходные
функции, часто «понимают»
плагины своих конкурентов.
Например, среди графических
приложений стандартом де-факто
стали уже упоминавшиеся плагины к
Photoshop, а среди приложений для работы
со звуком распространён формат VST.


Какие плагины писать?

Плагин зачастую включает в себя
такое множество сложных вещей, что
его можно считать самостоятельным
программным продуктом. В первую
очередь, это относится к
профессиональным плагинам для
сложных программ — например, к тому
же 3D Studio MAX. Чтобы создавать такие
плагины, нужна не только высокая
программистская квалификация
разработчика, но и хорошие знания в
той области, в которой работает
программа. Как правило, над
графическими и звуковыми фильтрами
работают команды из нескольких
человек, и большая часть времени
уходит не на написание кода, а на
математическое моделирование
преобразований, реализуемых в
плагине.

Но для некоторых программ плагины
можно писать и не будучи докой по
части эффектов Photoshop и
гармонического анализа. Множество
энтузиастов пишет свои плагины к
популярным пользовательским
программам — таким, как известная
программа для мгновенного обмена
сообщениями Miranda. Откровенно
говоря, «Мирандой» без
плагинов вообще пользоваться
довольно-таки затруднительно.
Чтобы написать свой плагин для неё,
зачастую достаточно знаний
школьника, интересующегося
программированием, да и вообще
любого человека, для которого
программирование — не основная
работа, а просто хобби.

Чтобы продемонстрировать вам
основные принципы написания
плагинов на практике, я расскажу,
как написать собственный
полнофункциональный плагин для
популярного файлового менеджера
Total Commander. Почему именно для него?
Потому что писать к нему плагины
достаточно просто, а сам Total Commander я
считаю лучшей из программ этого
класса.


Итак, пишем?

Писать плагины можно на любом
языке программирования, код на
котором можно скомпилировать и
скомпоновать в динамическую
библиотеку. Но сейчас для простоты
и наглядности я буду пользоваться
Delphi, поскольку с этим языком может
работать практически каждый, кто в
школе научился Паскалю. Да и сам Total
Commander, как известно, написан именно
на Delphi.

Плагины к «Командиру» бывают
разные. Очень разные, я бы даже
сказал. Всего их, на сегодняшний
день, четыре типа: плагины
встроенного архиватора, плагины
встроенного просмотрщика (Lister’а),
расширения файловой системы и
контент-плагины. Плагины для
архиватора позволяют работать
через Total Commander с новыми форматами
архивов как с обычными папками,
плагины Lister’а позволяют
просматривать по нажатию на кнопку
F3 файлы новых форматов. Плагины
файловой системы позволяют
работать со структурированными
хранилищами данных как с обычными
каталогами и файлами, лежащими на
диске. В принципе, они чем-то похожи
на архивные плагины.
Контент-плагины появились в Total
Commander’е сравнительно недавно,
начиная с версии 6.50 (на момент
написания статьи самой новой была
версия 7.01). Они позволяют
отображать дополнительную
информацию о разных файлах в
главном окне программы.

Естественно, разные виды плагинов
имеют разные принципы работы и
разную внутреннюю структуру.
Поэтому для дальнейших действий
стоит определиться с тем, какой
именно плагин мы с вами будем
писать. Все плагины хороши, но лично
мне кажется, что лучше всего
показать на примере плагин для
Lister’а, потому что всех форматов
файлов не перечислишь, да и новые
постоянно появляются. В общем,
написание плагинов к Lister’у —
полезный практический навык,
который может пригодиться в жизни.

Чтобы особо не мучиться с идеей
для нашего плагина, будем писать
плагин для показа RTF-файлов. Хоть
«Командир» это умеет и без
того, сейчас наша цель — научиться, а
не сотворить нечто поражающее
воображение.


Итак, пишем!

Для того, чтобы писать плагин,
нужна сначала некоторая
предварительная подготовка. Для
начала, наверное, неплохо бы
установить Delphi. Если она уже
установлена, то удалять и ставить
по-новому не надо. Кроме самой Delphi,
нужны заголовочные файлы, чтобы
экспортировать правильные функции
и найти, таким образом, общий язык с
Total Commander’ом. Скачать их нужно с
сайта Total Commander’а (ghisler.com/plugins.htm),
называется это «LS-Plugin writer’s guide».
Прямая ссылка такая: ghisler.fileburst.com/lsplugins/listplughelp1.5.zip,
но она, как видите, содержит в себе
номер версии, который может
измениться.

Ну вот, если вы всё скачали, то
можно переходить непосредственно к
действиям. Запустите среду Delphi и в
окне создания нового проекта
выберите «DLL» (в некоторых
версиях Delphi это называется «DLL
Wizard», но это не принципиально:
главная идея такова, что проект
должен быть проектом динамически
компонуемой библиотеки).

Один небольшой нюанс: поскольку
типов плагинов к TC несколько, им
всем принято давать разные
расширения. Плагины для Lister’а
традиционно имеют расширение WLX.
Поэтому лучше заранее сменить
расширение выходного файла в
настройках проекта.

В разделе uses главного файла
проекта нужно добавить модули Windows,
поскольку нам понадобятся
некоторые типы данных оттуда для
экспорта функций нашим плагином.
Всего же функций, которые нам
необходимо экспортировать, три. Вот
как они выглядят:

function ListLoad(ParentWin: thandle; FileToLoad: pchar;
 ShowFlags: integer): thandle; stdcall;
procedure ListCloseWindow(ListWin: thandle); stdcall;
procedure ListGetDetectString(DetectString: pchar;
maxlen: integer); stdcall;

Первая из них вызывает плагин для
работы. Её параметры — это
дескриптор окна Lister’а (с его помощью
мы будем поддерживать связь между
окном плагина и окном Lister’а), имя
загружаемого файла и комбинацию
настроек Lister’а. Но настройки нам
пока что не понадобятся, так что на
ShowFlags сейчас можно не обращать
внимания. Вторая функция (вернее, в
терминах Delphi это процедура)
вызывается, когда завершается
работа приложения, и требуется
закрытие окна. Её параметр — тоже
дескриптор окна Lister’а. Третья
функция позволяет Lister’у определить,
может ли плагин правильно
разобраться с переданным ему
файлом, или лучше его отобразить с
помощью какого-то другого плагина.
В общем-то, можно обойтись и без
второй по счёту функции, поскольку
в случае её отсутствия окно плагина
будет само уничтожено стандартными
средствами Windows API.

Добавьте в проект форму
(«File»->»New»->Form) и удалите
из файла формы глобальную
переменную Form1: TForm1. Особенности
написания DLL на Delphi таковы, что
глобальные переменные во время
этого процесса под запретом. На
форму положите компонент RichEdit (он
находится на вкладке «Win32»
палитры компонентов Delphi).
Установите его свойство Align на alClient
(для этого нужно использовать окно
Object Inspector, находящееся в левой части
среды).

Окно нашего плагина не должно
иметь рамки и заголовка, иначе окно
Lister’а будет выглядеть, мягко говоря,
странновато. Поэтому для создания
окна мы должны переопределить одну
процедуру. В секции Protected класса
окна нужно записать следующее:

procedure CreateParams(var Params: TCreateParams); override;

В теле процедуры пишем следующее:

inherited CreateParams(Params);
Params.Style := (WS_CHILD or WS_MAXIMIZE) and
not WS_CAPTION and not WS_BORDER;
Params.WindowClass.cbWndExtra := SizeOf(Pointer);

Первая строчка вызывает
унаследованный стандартный метод.
Вторая обрезает рамку окна, чтобы
оно не выглядывало из окна Lister’а
неподобающим образом. Третья
резервирует место для того, чтобы
мы могли потом уничтожить окно
(глобальными переменными
пользоваться нельзя — помните?).

Теперь можно перейти к
непосредственному заполнению тела
экспортируемых функций. Тело
процедуры ListDetectString будет коротким:

StrLCopy(DetectString, 'FORCE | EXT="RTF"', MaxLen);

Эта функция копирует строку
«FORCE | EXT=»RTF»» в переменную
DetectString. А строка сообщает Lister’у, что
мы будем открывать своим плагином
файлы с расширением «RTF» и при
этом переопределяем стандартную
функциональность просмотрщика.

Тело ListLoad будет уже гораздо более
длинным:

Var Form1: TForm1;
begin
 Result := 0;
 try
  if LowerCase(ExtractFileExt(string(FileToLoad))) <> '.rtf'
   then Exit;
  Form1 := TForm1.CreateParented(ListerWin);
  Form1.RichEdit1.Lines.LoadFromFile(string(FileToLoad));
  Form1.Show;
  SetWindowLong(Form1.Handle, GWL_USERDATA, Integer(@Form1));
  PostMessage(Form1.Handle, WM_SETFOCUS, 0, 0);
  Form1.RichEdit1.SetFocus;
  Result := Form1.Handle;
 except
 end;
end;

Переменная Form1 — это и есть,
собственно, окно плагина. Мы
проверяем расширение подсунутого
плагину файла, загружаем его,
показываем окно, сохраняем
указатель на него, чтобы при случае
изничтожить. Потом устанавливаем
фокус на наш RichEdit и возвращаем
дескриптор окна плагина. Поскольку
код находится в DLL-библиотеку,
помещаем всё внутрь блока try…except,
чтобы сбой плагина не привёл к сбою
«командира».

Код ListerCloseWindow таков:

Form1 := Pointer(GetWindowLong(PluginWin, GWL_USERDATA));
Form1.Close;
Form1.Free;

Получаем сохранённый адрес формы,
закрываем её и освобождаем память.
Нужно только не забыть объявить
переменную Form1 и «обернуть»
всё, что происходит внутри
процедуры try…except’ом.

Ну вот, осталось только
откомпилировать плагин и
установить его в Total Commander’е. По идее,
никаких сложностей с этим быть не
должно. Если вы заинтересовались
написанием плагинов к TC, то зайдите
на сайт русскоязычного сообщества
пользователей этой программы wincmd.ru. В разделе
«Статьи» вы найдёте материалы
по написанию плагинов на Delphi и C++, а
в разделе закачек есть
полнофункциональные плагины с
открытым исходным кодом.

Вадим СТАНКЕВИЧ

Разработка ПО - системы плагинов


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

Лишь став разработчиками можно понять, что такие модификации программ поддерживаются за счет концепции плагинов. Давайте разберемся в этой теме подробнее.


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

Плагины позволяют создавать подпрограммы, которые впоследствии подключаются или присоединяются к более крупной программе используя механизм hook (хук). Затем эти подпрограммы запускаются, изменяя или дополняя функции (поведение) запущенной программы.

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

Общие понятия

Системы плагинов бывают разных типов и форм. Разобраться в этом и проиллюстрировать их структуру помогут следующие основные концепции:

Концептуальные компоненты базовой системы плагинов

Программа

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

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

Хуки

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

Для запуска кода плагина используется хук. Реальными примерами могут служить обратные вызовы модели Rails или хуки жизненного цикла компонентов Vue.

Вот пример простого хука:

class RecordSaver
def save
writeToDisk()
onSaveHook()
end
def onSaveHook()
end
private
# ...разные функции
end

Видно, что у onSaveHook нет никакого поведения. В этом примере ожидается, что это поведение будет определено классом, расширяющим Record Saver и реализующим новое поведение в подклассе.

Плагины

Плагин — это код, который программисты пишут и “подключают” (plug in) для расширения возможностей программы.

Ниже представлена возможность расширения класса Record Saver из примера выше:

class LoggedRecordSaver < RecordSaver
def onSaveHook()
puts 'Saving record.'
end
end

Теперь после вызова Logged Record Saver#save() будет выводиться Saving record. после вызова #writeToDisk(). Это базовый вариант совершенствования функциональных свойств программ путем расширения RecordSaver.

Также следует отметить, что RecordSaver ничего не знает о LoggedRecordSaver, т. е. это однонаправленное взаимодействие.

Загрузчик

Плагин будет бесполезен, если его нельзя запустить. Но чтобы началось исполнение кода плагина, его нужно загрузить. Для этого есть несколько методов. Вот два из них:

  • Plugin-driven — управляемый плагином саморегистрируемый доступ к программе.
  • Program-driven — управляемый программой поиск и загрузка плагина.

В первом случае программа предоставляет метод для саморегистрации плагина в программе. Во втором случае программа находит плагин, например, загружая все файлы в папку с именем *_plugin или загружая манифест.


Создание чат-бота с плагинами

Чтобы можно было поболтать с чат-ботом Online или получать от него голосовые напоминания пришлось потратить на его создание свои выходные.

В чат-бот OneLine использованы плагины

Суть задачи проста: получить сообщение, затем произвольно преобразовать и отправить ответ. При этом была создана базовая система плагинов, которая реализует упомянутые выше концепции.

Загрузчик

Программная часть OneLine включает подмножество статических методов в модуле Core::Plugin, определяющее реализацию:

  • Загрузить plugin (#load);
  • Отслеживать все загруженные plugins (@@plugins);
  • Вызвать все загруженные plugins (#call_all).
module Core
module Plugin
@@plugins = {}
def self.plugins
return @@plugins
end
def load(plugin)
Core::Plugin.plugins[plugin.class] = plugin
end
def self.call_all(data, params = {})
plugin_responses = []
Core::Plugin.plugins.each { |key, plugin|
plugin_response = plugin.call(data, params)
plugin_responses << plugin_response if plugin_response
}
end
end

Обратите внимание, в этом примере загрузчик и хук объединены.

Загрузчик — это функция load, которую реализации Plugin могут вызывать для осведомления программы об этом.

Хук явно представляет собой функцию call_all, которая вызывается, когда программа получает сообщение.

Интерфейс плагина

Определяет ожидаемое поведение для всех реализаций plugins. В этом случае:

  • Проверка необходимости запуска плагина (#process?).
  • Метод, который будет вызываться для запуска плагина (#process).
  • Возвращаемый плагином стандартный ответ (#to_response).
module Core
module Plugin
def process?(data, params = {})
return false
end
def process(data, params = {})
end
def to_response(result)
return Core::PluginResponse.new(result)
end
end
end

Функция #process — вызывается хуком (при получении сообщения).

Реализации плагинов

Наконец, есть еще реализации плагинов, которые анализируют и интерпретируют сообщение, чтобы что-то с ним сделать. Я сделал плагины:

  • Расскажи мне анекдот (lib/oneline/jokes).
  • Следи за моим списком дел (lib/oneline/scheduler).
  • и многое другое!

Самый простой пример — это плагин, который сообщает текущее время.

module When
# Not called "Time" to prevent conflicts.
class Plugin
include Core::Plugin
def initialize(tasks = {})
load(self)
end
def process(text, params = {})
return {messages: ["It is #{Time.current.strftime('%b %e, %Y - %l:%M%P %Z')}."]}
end
def process?(text, params = {})
return text.downcase === 'time' if text.present?
end
end
::When::Plugin.new # Self-instantiates and registers the plugin
end

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

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

Читайте также:

  • Как правильно зарегистрировать плагин в nuxt.js
  • Введение в библиотеку Flutter Bloc
  • Искусство упрощения для программистов

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Joseph Gefroh: How to Design Software — Plugin Systems

В конце 2020 года мы делали проект со снежинками — писали специальный скрипт, который запускал падающий снег на сайтах. Если бы мы хотели сделать такой снег на любом своём сайте, это не составило бы труда: добавляешь скрипт в код страницы, и готово.

А вот на чужих сайтах была проблема. Скрипт нужно было вставлять через консоль. А если на сайте была настроена политика безопасности, которая запрещает запуск внешних скриптов, то магия не срабатывала. 

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

👉 Что такое расширение

Расширение для Chrome — это небольшая программа, которая выполняется внутри браузера и помогает расширить возможности сайтов. Сила расширения в том, что оно может выполняться прямо из меню браузера и не зависит от политик безопасности. 

Примеры того, что может сделать расширение: 

  • Сохранить ваши пароли и другие личные данные,
  • Выдрать из страницы скрытые данные (например, ссылку на скачивание музыки).
  • Менять что-то на странице, например, отключать рекламу, переводить текст, удалять ненужное.
  • Собирать статистику вашей работы в интернете.
  • Подключаться к другим сервисам (почте, чатам, файлообменникам) и взаимодействовать с ними из браузера.

В этой статье

Мы сделаем самое простое расширение для браузера Chrome, которое позволит запускать скрипт со снежинками на любом сайте, независимо от настроенной политики безопасности. Для этого воспользуемся официальным руководством Google по созданию расширений.

Манифест

В каждом расширении для браузера должен быть манифест — документ, в котором написано:

  • как называется расширение;
  • к чему расширение может получить доступ;
  • какие скрипты будут работать в фоне;
  • как должна выглядеть иконка расширения;
  • что показать или что выполнить, когда пользователь нажмёт на иконку расширения.

Манифест задаёт общие правила для всего расширения, поэтому манифест — единственный обязательный компонент. Можно обойтись без иконок и скриптов, но манифест обязательно должен быть.Каждый манифест хранится в файле manifest.json — создадим пустой файл с таким именем и напишем внутри такое:

{
«name»: «Запускаем снежинки на любом сайте»,
«description»: «Проект журнала Код»,
«version»: «1.0»,
«manifest_version»: 3
}

Первые две строчки — это название и подробное описание расширения. Третья отвечает за номер версии расширения, а последняя говорит браузеру, какая версия манифеста используется в описании. На момент выхода статьи в феврале 2021 года используется третья версия.

Сохраняем файл и всё, расширение готово. Оно ничего не умеет, ничего не делает, зато мы уже можем добавить его в браузер. Для этого запускаем Хром и в адресной строке пишем:

chrome://extensions/

Мы попадаем на страницу, которая нам покажет все установленные расширения:

Делаем своё расширение для браузера за 10 минут

Чтобы добавить своё расширение, в правом верхнем углу включаем режим разработчика, а затем нажимаем «Загрузить распакованное расширение»:

Делаем своё расширение для браузера за 10 минут

Теперь выбираем папку, в которой лежит наш манифест:

Делаем своё расширение для браузера за 10 минут

Отлично, мы только что добавили в браузер новое расширение:

Делаем своё расширение для браузера за 10 минут

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

Чтобы было проще работать и тестировать расширение, закрепим его на панели браузера:

Делаем своё расширение для браузера за 10 минут

Иконки

У расширения есть две иконки, которыми мы можем управлять: 

  1. Картинка в карточке расширения на странице настроек.
  2. Иконка на панели браузера.

Чтобы не рисовать всё с нуля, скачаем папку с иконками из того же руководства Google и положим её в ту же папку, что и манифест:

Теперь добавим иконки в манифест. За картинку в карточке отвечает блок icon, а за иконку на панели —  блок action. Разные размеры картинки нужны для того, чтобы на разных мониторах с любой плотностью пикселей иконки выглядели хорошо:

{
  "name": "Запускаем снежинки на любом сайте",
  "description": "Проект журнала Код",
  "version": "1.0",
  "manifest_version": 3,

  "action": {
    "default_icon": {
      "16": "/images/get_started16.png",
      "32": "/images/get_started32.png",
      "48": "/images/get_started48.png",
      "128": "/images/get_started128.png"
    }
  },
  "icons": {
    "16": "/images/get_started16.png",
    "32": "/images/get_started32.png",
    "48": "/images/get_started48.png",
    "128": "/images/get_started128.png"
  }
}

Сохраняем манифест, обновляем расширение на странице настроек и смотрим результат:

Добавляем иконки в манифест

Настраиваем разрешения

Разрешения — это то, что браузер позволяет делать расширению со страницами и с их содержимым. Для запуска снежинок нам нужно сделать две вещи:

  1. Понять, какая вкладка сейчас активная, чтобы запустить снежинки именно на ней.
  2. Запустить наш скрипт со снежинками.

Чтобы получить доступ к активной вкладке и к запуску скриптов, добавим в манифест такую строку:

"permissions": ["activeTab", "scripting"],

Показываем меню

Если мы сейчас нажмём на иконку расширения на панели браузера, то ничего не призойдёт, потому что мы ничего не запрограммировали. Исправим это — сделаем так, чтобы при нажатии расширение показывало кнопку запуска. Когда расширение будет уметь больше, вместо одной кнопки можно будет показать целое меню.

Чтобы сделать всплывающее меню, добавим в манифест в раздел action такую строку:

    "default_popup": "popup.html",

Она означает, что при нажатии на иконку мы увидим рядом с ней мини-страничку, на которой что-то будет.Создадим в той же папке расширения файл popup.html и добавим в него такой код:

<!DOCTYPE html>
<html lang="ru">
  <head>
  	<meta charset="UTF-8">
    <style type="text/css">

    	/* задаём размеры кнопки и размер текста на кнопке  */
    	button {
	      font-size: 12px;
		  height: 40px;
		  width: 80px;
		}
    </style>
  </head>
  <body>
  	<!-- создаём кнопку на странице -->
    <button id="snow">Запустить снежинки</button>
    <!-- подключаем скрипт, который обработает нажатие на эту кнопку -->
    <script src="popup.js"></script>
  </body>
</html>

Чтобы браузер не ругался, что у нас нет файла popup.js, создадим пустой файл с таким названием и положим его в ту же папку:

Показываем меню расширения

Сохраняем манифест, обновляем его на странице настроек и видим, что у нашего расширения появилось меню с кнопкой:

Показываем меню расширения

Запускаем снежинки

Вся магия будет происходить в файле popup.js — откроем его и добавим такой код:

// получаем доступ к кнопке
let snow = document.getElementById("snow");
// когда кнопка нажата — находим активную вкладку и запускаем нужную функцию
snow.addEventListener("click", async () => {
  // получаем доступ к активной вкладке
  let [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  // выполняем скрипт
  chrome.scripting.executeScript({
  	// скрипт будет выполняться во вкладке, которую нашли на предыдущем этапе
    target: { tabId: tab.id },
    // вызываем функцию, в которой лежит запуск снежинок
    function: snowFall,
  });
});

// запускаем снег
function snowFall() {
}

Последнее, что нам осталось сделать, — положить в функцию snowFall() полный код скрипта из проекта со снежинками и сохранить файл.

Проверка

В прошлый раз мы не смогли запустить скрипт на любой странице Яндекса — мешала политика безопасности. Теперь всё работает:

Проверяем расширение

Скачать упакованное расширение. Перед установкой его нужно распаковать в любую папку.


1. Вступление

За последние несколько лет

IntelliJ

от JetBrains быстро стал лучшей IDE для разработчиков Java. В нашем последнем отчете

State of Java

IntelliJ был IDE для 55% респондентов по сравнению с 48% годом ранее.

Одна особенность, которая делает IntelliJ такой привлекательной для разработчиков Java, — это возможность расширять и создавать новые функциональные возможности с помощью плагинов. В этом руководстве мы рассмотрим создание плагина IntelliJ, чтобы продемонстрировать несколько способов расширения IDE.

И обратите внимание, что хотя эта статья посвящена плагинам IntelliJ, все интегрированные среды разработки JetBrains имеют общий код. Поэтому

многие из методов, использованных здесь, могут быть применены к другим IDE JetBrain

, таким как PyCharm, RubyMine и другие.


2. Функциональность плагина

Функциональность плагина для IntelliJ обычно подпадает под одну из 4 категорий:


  • Пользовательские языки

    : способность писать, интерпретировать и компилировать код

написано на разных языках


Frameworks ** : поддержка сторонних фреймворков, таких как Spring


  • Инструменты

    : интеграция с внешними инструментами, такими как Gradle


  • Дополнения пользовательского интерфейса

    : новые пункты меню, окна инструментов и кнопки,

и больше

  • Плагины часто попадают в несколько категорий ** . Например, плагин

    Git

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


3. Создание плагина

ссылка:/uploads/intellij-plugin-100×66.jpg%20100w[]

  • Обратите внимание, что мы должны использовать JetBrains JDK ** , чтобы обеспечить наличие необходимых классов плагинов на пути к классам. IntelliJ должен поставляться с подходящим JDK по умолчанию, но если нет, мы можем скачать его с

    here

    .

На момент написания этой статьи

мы можем использовать только Java 8 для написания плагинов IntelliJ

. Это потому, что JetBrains в настоящее время не предоставляет официальный JDK для Java 9 или выше.


4. Пример плагина

Чтобы продемонстрировать написание плагина IntelliJ, мы создадим плагин, обеспечивающий быстрый доступ к популярному веб-сайту Stack Overflow из нескольких областей в среде IDE. Мы добавим:

  • Пункт меню Инструменты, чтобы посетить страницу Задать вопрос

  • Элемент всплывающего меню как в текстовом редакторе, так и в консоли для поиска

Переполнение стека для выделенного текста.


4.1. Создание действий

  • Действия являются основным компонентом, используемым для написания плагинов IntelliJ ** .

Действия инициируются событиями в IDE, такими как нажатие элемента меню или кнопки панели инструментов.

Первым шагом в создании действия является создание класса Java, который расширяет

AnAction

. Для нашего плагина Stack Overflow мы создадим 2 действия.

Первое действие открывает страницу «Задать вопрос» в новом окне браузера:

public class AskQuestionAction extends AnAction {
   @Override
   public void actionPerformed(AnActionEvent e) {
       BrowserUtil.browse("https://stackoverflow.com/questions/ask");
   }
}

Мы используем встроенный класс

BrowserUtil

, поскольку он обрабатывает все нюансы открытия веб-страницы в разных операционных системах и браузерах.

Второе действие открывает страницу поиска переполнения стека и передает текст поиска в виде строки запроса. На этот раз мы реализуем два метода.

Первый метод, который мы реализуем, похож на наше первое действие и обрабатывает открытие веб-браузера.

Однако сначала нам нужно собрать два значения для StackOverflow. Одним из них является языковой тег, а другим — текст для поиска.

Чтобы получить языковой тег, мы будем использовать

Program Structure Interface

.

Этот API анализирует все файлы в проекте и предоставляет программный способ их проверки.

В этом случае мы используем PSI для определения языка программирования файла:

PsiFile file = e.getData(CommonDataKeys.PSI__FILE);
Language lang = e.getData(CommonDataKeys.PSI__FILE).getLanguage();
String languageTag = "+[" + lang.getDisplayName().toLowerCase() + "]";

Обратите внимание, что PSI также предоставляет информацию о файле для конкретного языка.

Например,

мы могли бы использовать PSI для поиска всех открытых методов в классе Java.

Чтобы получить текст для поиска, мы будем использовать

__Editor

__API для получения выделенного текста на экране:

final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
CaretModel caretModel = editor.getCaretModel();
String selectedText = caretModel.getCurrentCaret().getSelectedText();

Хотя это действие одинаково для окон редактора и консоли, доступ к выделенному тексту работает одинаково.

Теперь мы можем собрать все это вместе в объявлении

actionPerformed

:

@Override
public void actionPerformed(AnActionEvent e) {

    PsiFile file = e.getData(CommonDataKeys.PSI__FILE);
    Language lang = e.getData(CommonDataKeys.PSI__FILE).getLanguage();
    String languageTag = "+[" + lang.getDisplayName().toLowerCase() + "]";

    Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
    CaretModel caretModel = editor.getCaretModel();
    String selectedText = caretModel.getCurrentCaret().getSelectedText()

    String query = selectedText.replace(' ', '+') + languageTag;
    BrowserUtil.browse("https://stackoverflow.com/search?q=" + query);
}

Это действие также отменяет второй метод с именем

update

. Это позволяет нам включать или отключать действие в различных условиях.

В этом случае мы отключаем действие поиска, когда нет выделенного текста:

@Override
public void update(AnActionEvent e) {
     Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
     CaretModel caretModel = editor.getCaretModel();
     e.getPresentation().setEnabledAndVisible(caretModel.getCurrentCaret().hasSelection());
}


4.2. Регистрация действий

Как только мы написали наши действия,

нам нужно зарегистрировать их в IDE

. Есть два способа сделать это.

Первый способ — использовать файл

plugin.xml

, который создается для нас, когда мы начинаем новый проект.

По умолчанию в файле будет пустой элемент

<actions>

, куда мы добавим наши действия:

<actions>
    <action
      id="StackOverflow.AskQuestion.ToolsMenu"
      class="com.baeldung.intellij.stackoverflowplugin.AskQuestionAction"
      text="Ask Question on Stack Overflow"
      description="Ask a Question on Stack Overflow">
        <add-to-group group-id="ToolsMenu" anchor="last"/>
    </action>
    <action
      id="StackOverflow.Search.Editor"
      class="com.baeldung.intellij.stackoverflowplugin.SearchAction"
      text="Search on Stack Overflow"
      description="Search on Stack Overflow">
        <add-to-group group-id="EditorPopupMenu" anchor="last"/>
    </action>
    <action
      id="StackOverflow.Search.Console"
      class="com.baeldung.intellij.stackoverflowplugin.SearchAction"
      text="Search on Stack Overflow"
      description="Search on Stack Overflow">
        <add-to-group group-id="ConsoleEditorPopupMenu" anchor="last"/>
    </action>
</actions>

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

Второй способ зарегистрировать действия — это программно использовать класс

ActionManager

:

ActionManager.getInstance().registerAction("StackOverflow.SearchAction", new SearchAction());

Это имеет то преимущество, что позволяет нам динамически регистрировать действия. Например, если мы напишем плагин для интеграции с удаленным API, нам может потребоваться зарегистрировать другой набор действий в зависимости от версии API, которую мы вызываем.

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

ApplicationComponent

для управления действиями, что требует больше кодирования и настройки XML.


5. Тестирование плагина

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

Мы можем вручную протестировать (и отладить) наш плагин, используя конфигурацию запуска плагина:

ссылка:/uploads/intellij-plugin-run-configuration-100×71.jpg%20100w[]

Это запустит новый экземпляр IntelliJ с активированным нашим плагином.

Это позволяет нам щелкать по различным пунктам меню, которые мы создали, и открывать соответствующие страницы переполнения стека.

Если вы хотите выполнить более традиционное модульное тестирование, IntelliJ предоставляет

headlessless environment

для запуска модульных тестов. Мы можем писать тесты, используя любую среду тестирования, какую захотим, и тесты выполняются с использованием реальных немодированных компонентов из IDE.


6. Развертывание плагина

Плагин DevKit предоставляет простой способ упаковки плагинов, чтобы мы могли их устанавливать и распространять. Просто щелкните правой кнопкой мыши по проекту плагина и выберите «Подготовить модуль плагина к развертыванию». Это сгенерирует файл JAR внутри директории проекта.

Сгенерированный файл JAR содержит код и файлы конфигурации, необходимые для загрузки в IntelliJ. Вы можете установить его локально или опубликовать в

plugin хранилище

для использования другими.

На снимке экрана ниже показан один из новых пунктов меню Stack Overflow в действии:

ссылка:/uploads/intellij-stackoverflow-pluginjpg-100×41.jpg%20100w[]


7. Заключение

В этой статье мы разработали простой плагин, который подчеркивает лишь некоторые из способов улучшения IntelliJ IDE.

Хотя мы в основном работали с действиями, плагин IntelliJ SDK предлагает несколько способов добавления новых функций в IDE. Для дальнейшего чтения, ознакомьтесь с

official руководством по началу работы

.

Как всегда, полный код нашего примера плагина можно найти в нашем

GitHub репозитории

.

Эта статья продолжает небольшую серию «Создаём ваш первый плагин для…», в которую уже вошли статьи про написания плагина для Grunt и Gulp.

Дисклеймер

Я люблю JavaScript. Мне довольно приятно наблюдать за тем, что этот прекрасный язык программирования вышел за пределы браузера и собирает всё больше и больше областей применения. Так, например, благодаря Electron от GitHub у меня появилось сразу несколько приложений, которые я использую в повседневной жизни. К таким приложениям относится Hain, 1Clipboard, Wagon, Gitify и, конечно же, Visual Studio Code.

Теперь поговорим о приятном для некоторых людей и противном для других. У меня нет симпатий к TypeScript. На то есть свои причины, в основном, связанные с типизацией — не люблю я её, что тут поделать. При этом я не буду отрицать, что когда-нибудь начну использовать TypeScript или подобный язык, компилируемый в JavaScript — всё бывает, мнение может меняться со временем. Однако, в связи с этим, в статье все примеры будут написаны на JavaScript, хотя сам редактор и вся документация к нему написана с применением TypeScript в примерах.

Кстати, я недавно узнал, что создателем TypeScript был Андерс Хейлсберг, который, оказывается, приложил руку к Turbo Pascal, Delphi и C#.

Что-то вместо введения

Герой нашего дня (VS Code) построен на Electron, который подробно рассматривался в статье «Построение Electron приложения. Введение». На момент написания статьи (июнь 2016) в основе редактора лежит Electron версии 0.37.6, что подразумевает под собой Chromium 49-ой ветки и Node.js версии 5.10.0. В репозитории на GitHub уже думают над переходом на новую версию Electron, где версия Chromium поднимется минимум до 51-ой ветки, а Node.js до версии 6.1.0 или выше. Всё это означает, что вы можете писать плагины, используя синтаксис ES2015 без Babel и его альтернатив, а также применяя любой API Node.js.

Предупреждение

Не стоит читать эту статью дальше введения, если вы не понимаете фишки, введённые в ES2015. Если говорить конкретно, то от вас требуется понимание деструктуризации, обещаний, стрелочных функций, const и let, а также понимание основ Node.js.

Итак, пожалуй, начнём с того, что плагины в VS Code изолированы от самого редактора и запускаются в отдельном хост-процессе (extension host process), который представляет собой процесс Node.js с возможностью использования VS Code API. Такой подход не позволяет плагинам влиять на производительность редактора при его запуске или в процессе его работы. Для пользователя это означает, что редактор не зависнет на время выполнения задач каким-либо плагином или, если плагин выдаст фатальную ошибку.

Для экономии расхода памяти разработчики также добавили ленивую загрузку плагинов. Это означает, что плагины активируются лишь в тот момент, когда они нужны. Например, если пользователь открывает Markdown-файл, то плагины, работающие с Markdown, будут загружены только в момент открытия файла. Разумеется, мы должны сообщить редактору о том, когда именно он должен активировать какой-либо плагин. О настройке ленивой загрузки мы поговорим позднее в разделе про файл манифеста.

Помимо всего прочего, плагины делятся на три вида в зависимости от функционала:

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

Ко второму виду относятся так называемые «языковые серверы». Клиентская часть плагина запускается в хост-процессе, а серверная часть создаёт дополнительный процесс, в котором производятся все сложные вычисления. К такому виду плагинов относятся линтеры.

К третьему виду относят «службы отладки», которые пишутся в виде отдельной программы и взаимодействуют с VS Code по специальному протоколу CDP (VS Code Debug Protocol).

В этой статье будет рассматриваться лишь первый вид плагинов на примере vscode-lebab. Во второй статье разбирается процесс построения второго вида плагинов на примере vscode-puglint.

Манифест плагина

Написание плагина начинается не с кода, а с файла манифеста, которым в мире Node.js является файл package.json. VS Code дополняет стандартный файл манифеста своими полями. Ниже будут рассмотрены самые основные из них.

publisher [string]

Имя пользователя, под которым вы зарегистрировались в vsce.

icon [string]

Путь до иконки, которая будет отображаться в магазине расширений. Размер иконки 128×128 пикселей. Также поддерживается SVG формат.

displayName [string]

Название плагина, которое будет отображаться в магазине расширений.

categories [array]

Массив, содержащий имена категорий, к которым относится плагин. Доступны следующие категории: [Languages, Snippets, Linters, Themes, Debuggers, Other]. Пожалуйста, указывайте категорию или категории обдуманно. Например, если ваше расширение включает в себя подсветку синтаксиса языка и сниппеты, то указывайте только эти две категории.

galleryBanner [object]

Настройки оформления страницы расширения в магазине. Используется для того, чтобы иконка расширения и фон подложки были контрастны. Свойство color отвечает за цвет фона, свойство theme за цвет шрифта: dark — белый, light — чёрный.

"galleryBanner": {
    "color": "#0000FF",
    "theme": "dark"
}

preview [boolean]

Флаг, позволяющий пометить плагин сообщением о том, что он доступен в режиме предварительного просмотра.

activationEvents [array]

Массив событий, в случае наступления которых плагин будет активирован редактором. Поддерживаются следующие события, активирующие плагин в том случае, если будет:

  • onLanguage — открыт файл указанного языка (не расширения).
  • onCommand — вызвана указанная команда.
  • onDebug — запущен сеанс отладки указанного типа.
  • workspaceContains — найден указанный файл в корневой папке проекта.

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

contributes [object]

С помощью этого поля в package.json разработчик описывает своё расширение. Доступно довольно большое количество всевозможных полей, описывающих:

  • configuration — поля, доступные пользователю в настройках редактора.
  • commands — команды, доступные пользователю в палитре команд F1.
  • keybindings — сочетания клавиш для вызова команд.
  • languages — языки.
  • debuggers — отладочный адаптер.
  • grammars — TextMate-грамматику, необходимую для подсветки синтаксиса.
  • themes — темы.
  • snippets — сниппеты.
  • jsonValidation — схемы проверки определённых JSON-файлов.

Несложно догадаться, что поля необходимо указывать в зависимости от типа вашего расширения. Если это плагин, сортирующий строки, то, вероятнее всего, вы будете использовать поля: configuration, commands и keybindings.

extensionDependencies [array]

Массив идентификаторов расширений, которые требуются для работы плагина. Например, если ваше расширение требует поддержки синтаксиса C#, то необходимо будет добавить в массив строку vscode.csharp, где vscode — ник опубликовавшего расширение, а csharp — имя расширения.

Немного про VS Code API

Как и полагается любому крупному проекту, VS Code имеет довольно обширный API, доступный разработчикам плагинов. Рассмотрим лишь так называемые пространства имён:

  • commands — это пространство имён для работы с командами. С помощью доступных методов разработчик может регистрировать, получать и выполнять команды.
  • env — пространство имён, содержащее описание переменных окружения редактора при запуске. С помощью соответствующих методов можно получить имя окна редактора, его язык, идентификатор редактора в системе и идентификатора сессии редактора, которые устанавливается при запуске.
  • extensions — пространство имён для работы с установленными расширениями. С помощью этого API можно получить все или конкретные расширения, известные редактору.
  • languages — пространство имён, позволяющее получить доступ к языковым возможностям редактора, например, к IntelliSense, подсказкам, а также функциям диагностики кода для линтеров.
  • window — пространство имён для работы с текущим окном редактора. Доступно API для работы с видимыми и активными окнами редактора, а также элементами пользовательского интерфейса. Последнее подразумевает под собой возможность отображения различных сообщений, ввода текста или выбора каких-либо вариантов.
  • workspace — пространство имён для работы с текущей рабочей областью, включая открытую директорию и файлы. С помощью этого API осуществляется вся работа с содержимым открытого файла.

Пишем стандартный плагин

Постановка задачи

В этой части статьи я буду описывать процесс написания плагина для VS Code на основе lebab, который автоматически конвертирует JavaScript-код, написанный по стандарту ES5 в ES2015. Это проект является альтернативой проекту Babel, но в обратную сторону.

Манифест

И снова скажу, что написание плагина начинается не с кода на JavaScript, а с манифеста. Первым делом создаём файл package.json и пишем туда пару десятков строк, описывающих плагин. Полный листинг манифеста вы сможете найти в репозитории плагина vscode-lebab. Остановимся именно на тех моментах, которые касаются работы с VS Code.

Во-первых, укажем информацию, которая будет отображаться в маркете:

{
  "displayName": "lebab",
  "publisher": "mrmlnc",
  "icon": "icon.png",
  "homepage": "https://github.com/mrmlnc/vscode-lebab/blob/master/README.md",
  "categories": [
    "Other"
  ]
}

Во-вторых, укажем массив событий, на которые наш плагин должен откликаться. Про команду lebab.convert я расскажу немного позднее.

{
  "activationEvents": [
    "onCommand:lebab.convert"
  ]
}

В-третьих, опишем плагин. Предполагается, что пользователю будет доступна лишь одна команда, по вызову которой он получит сконвертированный ES5-код в синтаксис ES2015. Также предполагается, что в настройках редактора пользователь сможет указать, что именно он хочет конвертировать с помощью lebab. Для этого я определил опцию lebab.transforms, содержащую объект ключей, с которыми будет работать конвертер.

{
  "contributes": {

    "commands": [{
      "command": "lebab.convert",
      "title": "Lebab: convert JavaScript code from ES5 to ES2015"
    }],

    "configuration": {
      "type": "object",
      "title": "Lebab configuration",
      "properties": {
        "lebab.transforms": {
          "type": "object",
          "default": {},
          "description": "Convert your old-fashioned code with a specific transformation."
        }
      }
    }

  }
}

Убираем из маркета лишнее

Сколько раз не говори, но я всё равно встречаю модули в npm, у которых вместе с кодом я получаю файлы тестов, изображений и прочей лабуды. К счастью, теперь я могу ссылаться на эту статью в отношении npm. В случае VS Code, необходимо создать файл .vscodeignore, который действует так же, как и файл .gitignore, но в отношении маркета расширений.

У меня файл .vscodeignore имеет следующее содержимое:

.vscode/**
typings/**
test/**
.editorconfig
.gitignore
.travis.yml
jsconfig.json

Я очень прошу, убирайте всё лишнее из плагина, иначе я вас вычислю по IP и накажу.

Базовый код

В мире Node.js принято писать код модуля в файле index.js, а приложения — app.js. Мир VS Code тоже имеет традиции, и код плагина пишется в файле extension.js.

Внимание

Если очень хочется писать код в файле с именем, отличным от extension.js, то, как и в мире Node.js, вам придётся указать имя файла в поле main в манифесте.

Для начала определим две функции. Первая функция будет иметь имя activate и вызываться в том случае, если плагин был активирован событием, указанным в манифесте. Вторая функция имеет имя deactivate и вызывается в том случае, если плагин был деактивирован. Под деактивацией следует понимать последействие команды, а не удаление плагина. Её предназначение в большинстве плагинов излишне, поэтому она не обязательна. Далее в статье я не буду упоминать функцию деактивации плагина.

const vscode = require('vscode');

function activate(context) {
  // Code...
}

exports.activate = activate;

function deactivate() {
  // Code...
}

exports.deactivate = deactivate;

Напомню, что в файле манифеста была указана команда lebab.convert — самое время её зарегистрировать. Для регистрации команд существует два метода:

  • registerTextEditorCommand — регистрирует команду в контексте текстового редактора или файла.
  • registerCommand — регистрирует команду в глобальном контексте, то есть вне зависимости от наличия открытого редатора с текстом.

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

В конце все объявленные команды должны быть добавлены в массив subscriptions.

'use strict';

const vscode = require('vscode');

function activate(context) {
  const convert = vscode.commands.registerTextEditorCommand('lebab.convert', (textEditor) => {
    // ...
  });

  context.subscriptions.push(convert);
}

exports.activate = activate;

При регистрации команды необходимо передать идентификатор команды, указанный в файле манифеста и функцию обратного вызова, в которую передаётся объект textEditor, содержащий всю информацию о текущем редакторе, такую как:

  • Файл на диске или новый файл (untitled)
  • Путь до файла и его имя
  • Идентификатор языка
  • Наличие EOL
  • Если было выделение текста, то параметры этого выделения
  • Статистика текста (количество символов в строке и прочее)
  • Версия файла (проще говоря, номер сохранения в истории файла)
  • Строки файла
  • и т.д.

Чисто практически, вы можете обращаться к свойствам объекта и работать с полученными из него данными. Но, разумеется, лучше всего обращаться к этому объекту с помощью определённых методов. На данном этапе нам нужно получить текст файла, чтобы в дальнейшем обработать его, и настройки, определённые в редакторе для нашего плагина.

Получить текст открытого документа можно, используя метод getText, а настройки, используя метод getConfiguration у workspace API:

const convert = vscode.commands.registerTextEditorCommand('lebab.convert', (textEditor) => {
  // Обычный объект, где имена свойств совпадают с теми, что были обозначены в манифесте.
  const options = vscode.workspace.getConfiguration('lebab');

  // Текст открытого файла.
  const text = textEditor.document.getText();
});

Внимание

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

Далее я не буду рассматривать процесс вызова Lebab, потому что это элементарное действие, не относящееся к VS Code. Покажу лишь тот участок, что отвечает за вставку обработанного текста обратно в окно редактора. Для этого мы обратимся к объекту textEditor и вызовем метод edit с коллбэком, представленным ниже. Конечно же, код должен располагаться после получения текста документа и настроек редактора в функции обратного вызова регистрации команды.

textEditor.edit((editBuilder) => {
  // Получаем текущий документ.
  const document = textEditor.document;

  // Получаем последнюю строку документа.
  const lastLine = document.lineAt(document.lineCount - 1);

  // Создаём нулевую позицию, то есть начало документа, где первый ноль — номер
  // строки, а второй ноль — номер символа в строке.
  const start = new vscode.Position(0, 0);

  // Создаём завершающую позицию, где первое число — последняя строка документа,
  // а второе — номер последнего символа в строке.
  const end = new vscode.Position(document.lineCount - 1, lastLine.text.length);

  // Создаём диапазон, используя специальное API.
  const range = new vscode.Range(start, end);

  // Заменяем текст в обозначенном диапазоне на что-либо.
  editBuilder.replace(range, text);
});

Собственно, это всё, что требуется сделать в обычном плагине для VS Code: получить текст, обработать его и вернуть обратно. Полный листинг кода содержит 52 строки и размещён на GitHub в репозитории vscode-lebab.

Как можно заметить, ничего сложно — простейший JavaScript-код, который разбавлен вызовами необходимых API редактора. В этой статье был рассмотрен простейший случай, когда у вас есть готовое решение и его нужно подружить с редактором. В случае, если готового решения нет, то лучше оформить его в виде npm-пакета по всем канонам (тесты, документация), а уже потом подружить его с редактором. В качестве примеров, если решите писать плагин на JavaScript, вы можете посмотреть код следующих плагинов:

  • vscode-lebab
  • vscode-attrs-sorter
  • vscode-csscomb
  • vscode-postcss-sorting
  • vscode-stylefmt

Что-то вместо вывода

В этой статье я помог вам начать писать плагины для VS Code. Многое осталось за кулисами и не рассматривалось, однако вам в любом случае придётся обращаться к документации. Считайте, что эта статья преследует цель показать, что писать плагин для VS Code довольно просто, причём необязательно делать это на TypeScript. Хотя, при этом не стоит забывать, что TypeScript — это всё тот же JavaScript.

Также, советую посмотреть на код, представленный в репозитории VSCode-Sample. Здесь автор собрал примеры взаимодействия с UI редактора, которые, возможно, помогут вам освоиться.

Если вам интересна тема разработки плагинов для VS Code, то добро пожаловать во вторую часть этой статьи, в которой рассматривается процесс написания плагина, использующего языковой сервер.

Что почитать?

  • Конечно, документацию:
    • Обзор экосистемы расширений
    • Обзор API
    • Про публикацию расширений
    • Про утилиты, упрощающие создание расширений
    • Примеры
  • Visual Studio Code Extensions: Editing the Document
  • Getting Input and Displaying Output in a Visual Studio Code Extension

Sublime Text 2 — это настраиваемый текстовый редактор, который все больше привлекает внимание кодеров, ищущих мощный, быстрый и современный инструмент. Сегодня мы собираемся воссоздать мой популярный плагин Sublime, который
отправляет CSS через API Nettuts + Prefixr для упрощенного
кросс-браузерного CSS.

Когда закончите, у вас будет четкое представление о том, как написан плагин Sublime Prefixr, и быть готовым начать писать свои собственные плагины для редактора!


Предисловие: терминология и справочный материал

Модель расширения для Sublime Text 2 довольно полнофункциональна.

Модель расширения для Sublime Text 2 довольно полнофункциональна. Есть способы изменить подсветку синтаксиса, фактический хром редактора и все меню. Кроме того, можно создавать новые системы сборки, автозавершения, определения языков, фрагменты, макросы, привязки клавиш, привязки и плагины мыши. Все эти различные типы модификаций реализуются через файлы, которые организованы в пакеты.

Пакет — это папка, которая хранится в вашем каталоге Packages. Вы можете получить доступ к своему каталогу пакетов, нажав на пункт меню Preferences > Browse Packages …. Также возможно объединить пакет в один файл, создав zip-файл и изменив расширение на .sublime-package.  В этом уроке мы обсудим несколько дополнительных вопросов.

Sublime поставляется в комплекте с множеством различных пакетов. Большинство пакетов в комплекте имеют специфику языка. Они содержат определения языков, автодоставки и системы сборки. В дополнение к языковым пакетам есть еще два пакета: Default и User. Пакет
Default (по умолчанию) содержит все стандартные привязки клавиш, определения меню, настройки файлов и целую кучу плагинов, написанных на Python. Пакет User отличается тем, что он всегда загружен последним. Это позволяет пользователям переопределять значения по умолчанию, настраивая файлы в своем User пакете.

Во время написания плагина ссылка Sublime Text 2 API будет иметь важное значение.

Во время написания плагина ссылка Sublime Text 2 API будет иметь важное значение. Кроме того, пакет Default служит хорошей ссылкой для выяснения того, как делать вещи и что возможно. Большая часть функциональности редактора отображается через команды. Любая операция, отличная от ввода символов, выполняется с помощью команд. Просмотрев вкладку Preferences> Key Bindings — Defaultmenu, вы можете найти сокровищницу встроенных функций.

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


Шаг 1 — Запуск плагина

Sublime поставляется с функциональностью, которая генерирует каркас кода Python, необходимый для написания простого плагина. Выберите пункт меню Tools > New Plugin, и новый буфер будет открыт с помощью этого шаблона.

import sublime, sublime_plugin

class ExampleCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        self.view.insert(edit, 0, "Hello, World!")

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

Когда мы сохраним файл, мы создадим новый пакет для его сохранения. Нажмите ctrl+s (Windows / Linux) или cmd+s (OS X), чтобы сохранить файл. Диалоговое окно сохранения откроется для пакета User. Не сохраняйте файл там, но вместо этого найдите папку и создайте новую папку с именем Prefixr.

Packages/
…
- OCaml/
- Perl/
- PHP/
- Prefixr/
- Python/
- R/
- Rails/
…

Теперь сохраните файл внутри папки Prefixr как Prefixr.py. На самом деле не имеет значения, что такое имя файла, просто оно заканчивается на .py. Однако по соглашению мы будем использовать имя плагина для имени файла.

Теперь, когда плагин сохранен, давайте попробуем его. Откройте консоль Sublime, нажав ctrl + `. Это консоль Python, которая имеет доступ к API. Введите следующий Python, чтобы проверить новый плагин:

view.run_command('example')

Вы должны увидеть, что Hello World вставлен в начало файла плагина. Обязательно отмените это изменение, прежде чем мы продолжим.


Шаг 2 — Типы команд и присвоение имен

Для плагинов Sublime предоставляет три разных типа команд.

  •  Текстовые команды обеспечивают доступ к содержимому выбранного файла/буфера через объект View
  • Команды окна предоставляют ссылки на текущее окно через объект Window
  • Команды приложения не имеют ссылки на какое-либо конкретное окно или файл/буфер и используются реже

Поскольку мы будем манипулировать содержимым файла/буфера CSS с помощью этого плагина, мы собираемся использовать класс sublime_plugin.TextCommand в качестве основы для нашей пользовательской команды Prefixr. Это приводит нас к теме названия классов команд.

В каркасе плагина, предоставленном Sublime, вы заметите класс:

class ExampleCommand(sublime_plugin.TextCommand):

Когда мы хотели запустить команду, мы выполнили следующий код в консоли

view.run_command('example')

Sublime будет принимать любой класс, который расширяет один из классов sublime_plugin
(TextCommand, WindowCommand или ApplicationCommand), удалите Command суффикса, а затем преобразуйте значение CamelCaseinto в underscore_notation  для имени команды.

Таким образом, для создания команды с именем prefixr, класс должен быть PrefixrCommand.

class PrefixrCommand(sublime_plugin.TextCommand):

Шаг 3 — Выбор текста   

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

Теперь, когда мы правильно включили наш плагин, мы можем начать процесс присвоения CSS из текущего буфера и отправки его в API Prefixr. Одной из наиболее полезных функций Sublime является возможность множественного выбора. Когда мы присваиваем выделенный текст, нам нужно написать наш плагин в ручку не только для первого выбора, но и для всех.

Поскольку мы пишем текстовую команду, у нас есть доступ к текущему представлению через self.view. Метод sel() объекта View возвращает итерируемый RegionSet текущего выбора. Начнем с сканирования этих фигурных скобок. Если фигурных скобок нет, мы можем развернуть выделение на окружающие фигурные скобки, чтобы обеспечить префикс целого блока. Независимо от того, был ли наш выбор включенными фигурными фигурными скобками, также будет полезно узнать, можем ли мы настроить прописные и форматирование на результат, который мы получаем от  Prefixr API.

braces = False
sels = self.view.sel()
for sel in sels:
    if self.view.substr(sel).find('{') != -1:
        braces = True

Этот код заменяет содержимое метода каркаса run().

Если мы не нашли фигурных скобок, мы прокручиваем каждый выбор и корректируем выбор ближайшего закрытия фигурных скобок. Затем мы используем встроенную команду expand_selection с аргументом to в brackets, чтобы убедиться, что у вас есть полное содержимое каждого выбранного блока CSS.

if not braces:
    new_sels = []
    for sel in sels:
        new_sels.append(self.view.find('}', sel.end()))
    sels.clear()
    for sel in new_sels:
        sels.add(sel)
    self.view.run_command("expand_selection", {"to": "brackets"})

Если вы хотите проверить вашу работу до сих пор, сравните источник с файлом Prefixr-1.py в zip-файле исходного кода.


Шаг 4 — Работа потоков

Чтобы предотвратить плохое соединение от прерывания другой работы, нам нужно убедиться, что вызовы API Prefixr происходят в фоновом режиме.

На этом этапе выбор был расширен, чтобы получить полное содержимое каждого блока CSS. Теперь нам нужно отправить их в Prefixr API. Это простой HTTP-запрос, который мы будем использовать для модулей urllib и urllib2. Тем не менее, прежде чем мы начнем снимать веб-запросы, нам нужно подумать о том, как потенциально отстающий веб-запрос может повлиять на производительность редактора. Если по какой-либо причине пользователь работает с высокой задержкой или медленным подключением, запросы к Prefixr API могут легко занять пару секунд или более.

Чтобы предотвратить плохое соединение от прерывания другой работы, нам нужно убедиться, что вызовы Prefixr API происходят в фоновом режиме. Если вы ничего не знаете о потоковой передаче, очень простое объяснение заключается в том, что потоки — это способ для программы планировать несколько наборов кода для запуска, казалось бы, в одно и то же время. Это важно в нашем случае, потому что он позволяет коду, который отправляет данные и ожидает ответа от Prefixr API, препятствует замораживанию остальной части пользовательского интерфейса Sublime.


Шаг 5 — Создание потоков

Мы будем использовать Python threading module для создания потоков. Чтобы использовать модуль потоковой передачи, мы создаем новый класс, который расширяет threading.Thread, называемый PrefixrApiCall. Классы, которые расширяют threading.Thread включают метод run(), который содержит весь код для выполнения в потоке.

class PrefixrApiCall(threading.Thread):
    def __init__(self, sel, string, timeout):
        self.sel = sel
        self.original = string
        self.timeout = timeout
        self.result = None
        threading.Thread.__init__(self)

    def run(self):
        try:
            data = urllib.urlencode({'css': self.original})
            request = urllib2.Request('http://prefixr.com/api/index.php', data,
                headers={"User-Agent": "Sublime Prefixr"})
            http_file = urllib2.urlopen(request, timeout=self.timeout)
            self.result = http_file.read()
            return

        except (urllib2.HTTPError) as (e):
            err = '%s: HTTP error %s contacting API' % (__name__, str(e.code))
        except (urllib2.URLError) as (e):
            err = '%s: URL error %s contacting API' % (__name__, str(e.reason))

        sublime.error_message(err)
        self.result = False

Здесь мы используем метод __init __ () потока, чтобы установить все значения, которые понадобятся во время веб-запроса. Метод run() содержит код для настройки и выполнения HTTP-запроса для Prefixr API. Поскольку потоки работают одновременно с другим кодом, невозможно напрямую вернуть значения. Вместо этого мы устанавливаем self.result на результат вызова.

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

import urllib
import urllib2
import threading

Теперь, когда у нас есть потоковый класс для выполнения HTTP-вызовов, нам нужно создать поток для каждого выбора. Для этого мы возвращаемся к методу run() нашего класса PrefixrCommand и используем следующий цикл:

threads = []
for sel in sels:
    string = self.view.substr(sel)
    thread = PrefixrApiCall(sel, string, 5)
    threads.append(thread)
    thread.start()

Мы отслеживаем каждый поток, который мы создаем, а затем вызываем метод start() для запуска каждого из них.

Если вы хотите проверить вашу работу до сих пор, сравните источник с файлом Prefixr-2.py в zip-файле исходного кода.


Шаг 6 — Подготовка к результатам

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

Во-первых, мы очищаем все выборки, потому что раньше мы их модифицировали. Позже мы вернем их в разумное состояние.

self.view.sel().clear()

Кроме того, мы запускаем новый объект Edit. Эта группа операций для отмены и повтора. Мы указываем, что мы создаем группу для команды prefixr.

edit = self.view.begin_edit('prefixr')

В качестве последнего шага мы вызываем метод, который будем писать далее, который будет обрабатывать результат запросов API.

self.handle_threads(edit, threads, braces)

Шаг 7 — Обработка потоков

На этом этапе наши потоки работают или, возможно, даже завершены. Затем нам нужно реализовать метод handle_threads(), на который мы только что ссылались. Этот метод будет проходить через список потоков и искать потоки, которые больше не работают.

def handle_threads(self, edit, threads, braces, offset=0, i=0, dir=1):
    next_threads = []
    for thread in threads:
        if thread.is_alive():
            next_threads.append(thread)
            continue
        if thread.result == False:
            continue
        offset = self.replace(edit, thread, braces, offset)
    threads = next_threads

Если поток все еще работает, добавим его в список потоков, чтобы проверить его позже. Если результат был неудачным, мы игнорируем его, однако для хороших результатов мы вызываем новый метод replace(), который мы будем писать в ближайшее время.

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

if len(threads):
    # This animates a little activity indicator in the status area
    before = i % 8
    after = (7) - before
    if not after:
        dir = -1
    if not before:
        dir = 1
    i += dir
    self.view.set_status('prefixr', 'Prefixr [%s=%s]' % 
        (' ' * before, ' ' * after))

    sublime.set_timeout(lambda: self.handle_threads(edit, threads,
        braces, offset, i, dir), 100)
    return

В первом разделе кода используется простое целочисленное значение, хранящееся в переменной i,  для перемещения = назад и вперед между двумя скобками. Последняя часть является самой важной. Это говорит Sublime снова запустить метод handle_threads() с новыми значениями за 100 миллисекунд. Это похоже на функцию setTimeout() в JavaScript.

Ключевое слово lambda — это функция Python, которая позволяет нам создать новую неназванную или анонимную функцию

Метод sublime.set_timeout() требует функции или метода и количества миллисекунд, пока он не будет выполнен. Без lambda мы могли бы сказать,
что мы хотели запустить handle_threads(), но мы не смогли бы указать
параметры.

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

self.view.end_edit(edit)

self.view.erase_status('prefixr')
selections = len(self.view.sel())
sublime.status_message('Prefixr successfully run on %s selection%s' %
    (selections, '' if selections == 1 else 's'))

Если вы хотите проверить вашу работу до сих пор, сравните источник с файлом Prefixr-3.py в zip-файле исходного кода.


Шаг 8 — Выполнение замены

При обработке наших потоков нам просто нужно написать код, который
заменяет исходный CSS результатом из  Prefixr API. Как мы упоминали ранее, мы собираемся написать метод под названием replace().

Этот метод принимает ряд параметров, включая объект Edit для отмены, поток, который захватил результат из API Prefixr, если исходный выбор включал фигурные скобки и, наконец, смещение выбора.

def replace(self, edit, thread, braces, offset):
    sel = thread.sel
    original = thread.original
    result = thread.result

    # Here we adjust each selection for any text we have already inserted
    if offset:
        sel = sublime.Region(sel.begin() + offset,
            sel.end() + offset)

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

Следующим шагом будет подготовка результата из Prefixr API, который нужно удалить в качестве замены CSS. Это включает в себя преобразование окончаний строк и отступов в соответствии с текущим документом и исходным выбором.

result = self.normalize_line_endings(result)
(prefix, main, suffix) = self.fix_whitespace(original, result, sel,
    braces)
self.view.replace(edit, sel, prefix + main + suffix)

В качестве последнего шага мы устанавливаем выбор пользователя для включения конца последней строки нового CSS, который мы вставили, а затем возвращаем отрегулированное смещение для использования для любых последующих выборов.

end_point = sel.begin() + len(prefix) + len(main)
self.view.sel().add(sublime.Region(end_point, end_point))

return offset + len(prefix + main + suffix) - len(original)

Если вы хотите проверить свою работу до сих пор, сравните источник с файлом Prefixr-4.py в zip-файле исходного кода.


Шаг 9 — Манипуляция пробелами

Мы использовали два пользовательских метода во время процесса замены, чтобы подготовить новый CSS для документа. Эти методы принимают результат Prefixr и изменяют его в соответствии с текущим документом.

normalize_line_endings() берет строку и гарантирует соответствие строк окончанию текущего файла. Мы используем класс Settings из Sublime API для получения правильных окончаний строк.

def normalize_line_endings(self, string):
    string = string.replace('rn', 'n').replace('r', 'n')
    line_endings = self.view.settings().get('default_line_ending')
    if line_endings == 'windows':
        string = string.replace('n', 'rn')
    elif line_endings == 'mac':
        string = string.replace('n', 'r')
    return string

Метод fix_whitespace() немного сложнее, но выполняет те же манипуляции, что и для отступов и пробелов в блоке CSS. Эта манипуляция работает только с одним блоком CSS, поэтому мы выходим, если одна или несколько фигурных скобок были включены в исходный выбор.

def fix_whitespace(self, original, prefixed, sel, braces):
    # If braces are present we can do all of the whitespace magic
    if braces:
        return ('', prefixed, '')

В противном случае мы начнем с определения уровня отступа исходного CSS. Это делается путем поиска пробелов в начале выбора. Это делается путем поиска пробелов в начале выбора.

(row, col) = self.view.rowcol(sel.begin())
indent_region = self.view.find('^s+', self.view.text_point(row, 0))
if self.view.rowcol(indent_region.begin())[0] == row:
    indent = self.view.substr(indent_region)
else:
    indent = ''

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

prefixed = prefixed.strip()
prefixed = re.sub(re.compile('^s+', re.M), '', prefixed)

settings = self.view.settings()
use_spaces = settings.get('translate_tabs_to_spaces')
tab_size = int(settings.get('tab_size', 8))
indent_characters = 't'
if use_spaces:
    indent_characters = ' ' * tab_size
prefixed = prefixed.replace('n', 'n' + indent + indent_characters)

Мы заканчиваем метод, используя исходное начальное и конечное пустое пространство, чтобы гарантировать, что новый префикс CSS подходит именно к оригиналу.

match = re.search('^(s*)', original)
prefix = match.groups()[0]
match = re.search('(s*)Z', original)
suffix = match.groups()[0]

return (prefix, prefixed, suffix)

С помощью метода fix_whitespace() мы использовали (re)модуль регулярного выражения Python, поэтому нам нужно добавить его в список импорта в верхней части скрипта.

import re

И с этим мы завершили процесс написания команды prefixr. Следующий шаг — сделать команду легкой для запуска, предоставив комбинацию клавиш и запись в меню.


Шаг 10 — Привязки клавиш

Большинство настроек и модификаций, которые могут быть сделаны для Sublime, выполняются через файлы JSON, и это верно для привязки клавиш. Привязка клавиш обычно зависит от ОС, а это значит, что для вашего плагина необходимо создать три файла привязки ключей. Файлы должны быть названы Deafult (Windows).sublime-keymap, Default (Linux).sublime-keymap и Default (OSX).sublime-keymap.

Prefixr/
...
- Default (Linux).sublime-keymap
- Default (OSX).sublime-keymap
- Default (Windows).sublime-keymap
- Prefixr.py

Файлы .sublime-keymap содержат массив JSON, который содержит объекты JSON для указания привязок клавиш. Объекты JSON должны содержатьkeys и command ключ и могут также содержать ключ args, если команда требует аргументов. Самое сложное в выборе привязки ключей — это то, что привязка ключей еще не используется. Это можно сделать, перейдя в меню Preferences > Key Bindings – Default и поиск ключевого слова, который вы хотите использовать. Как только вы найдете подходящую неиспользуемую привязку, добавьте ее в .sublime-keymap файл.

[
  { 
		"keys": ["ctrl+alt+x"], "command": "prefixr" 
	}
]

Обычно привязки клавиш Linux и Windows одинаковы. Ключ cmd в OS X указан строкой super в .sublime-keymap файлах. При портировании привязки ключей к операционным системам обычно используется ключ ctrl на Windows и Linux для super на OS X. Однако это может быть не самым естественным движением руки, поэтому, если возможно, попробуйте и проверьте ваши комбинации клавиш на реальной клавиатуре.


Шаг 11 — Записи в меню

Одна из более классных вещей в расширении Sublime заключается в том, что можно добавлять элементы в структуру меню, создавая .sublime-menu файлы. Файлы меню должны быть названы конкретными именами, чтобы указать, на какое меню они влияют:

  • Main.sublime-menu управляет основным программным меню
  • Side Bar.sublime-menu управляет щелчком правой кнопки мыши по файлу или папке на боковой панели
  • Context.sublime-menu управляет щелчком правой кнопки мыши по редактируемому файлу

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

Для Prefixr мы хотим добавить пункт меню в меню Edit и некоторые записи в меню Preferences для настроек. Следующий пример — структура JSON для записи меню Edit. Я опустил записи в меню Preferences, так как они довольно многословны, вложенные на несколько уровней.

[
{
	"id": "edit",
	"children":
	[
	    {"id": "wrap"},
	    { "command": "prefixr" }
	]
}
]

Один кусок, на который нужно обратить внимание, — это id ключи. Указав id существующей записи меню, можно добавить запись без переопределения существующей структуры. Если вы откроете файл Main.sublime-menu из пакета Default и просмотрите его, вы можете определить, в какой id хотите добавить вашу запись.

На данный момент ваш пакет Prefixr должен выглядеть почти идентично официальной версии GitHub.


Шаг 12 — Распределение пакета

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

Sublime поддерживает распространение zip-файла каталога пакетов в качестве простого способа совместного использования пакетов. Просто закройте папку вашего пакета и измените расширение на .sublime-package. Теперь другие пользователи могут поместить это в свой каталог установленных пакетов и перезапустить Sublime для установки пакета.

Наряду с легкой доступностью для большого количества пользователей, наличие пакета через пакет управления пакетами позволяет пользователям автоматически обновляться до последних обновлений.

Хотя это, безусловно, может работать, есть также менеджер пакетов для Sublime, называемый Package Control, который поддерживает основной список пакетов и автоматическое обновление. Чтобы ваш пакет был добавлен к каналу по умолчанию, просто запустите его на GitHub или BitBucket, а затем разветвите файл канала (на GitHub или BitBucket), добавьте репозиторий и отправьте запрос на перенос. После принятия запроса на перенос ваш пакет будет доступен тысячам пользователей, использующих Sublime. Наряду с легкой доступностью для большого количества пользователей, наличие пакета через пакет управления пакетами позволяет пользователям автоматически обновляться до последних обновлений.

Если вы не хотите размещать на GitHub или BitBucket, существует система custom/JONON channel/repository, которую можно использовать для размещения в любом месте, при этом все же предоставляя пакет всем пользователям. Он также предоставляет расширенные функции, такие как указание доступности пакетов ОС. Дополнительную информацию см. На странице PackageControl.


Идите напишите несколько плагинов!

Теперь, когда мы рассмотрели шаги, чтобы написать плагин Sublime, пришло время для вас погрузиться! Сообщество плагинов Sublime создает и публикует новые функции почти каждый день. С каждым выпуском Sublime становится все более мощным и универсальным. Sublime Text Forum — отличное место, где можно получить помощь и поговорить с другими о том, что вы создаете.

Многие программные продукты предоставляют возможность расширить функционал путём установки плагинов, а соответствующая документация и/или статьи сообщества подробно рассказывают, как создать свой первый плагин к %PRODUCT_NAME%. Но не в этой статье. Здесь будут созданы свои велосипеды с плагинами в образовательных целях.

Предисловие

Первая мысль о собственной плагинной системе возникла ещё в четвёртом семестре, на курсе Языков Системного Программирования. Лабораторные работы принимали такие же студенты, как и мы, только на год старше и прошедшие с отличием курс ЯСПа. Ключевым во время сдачи лаб изучить что-то новое, а особо умные студенты, желающие получить оценку выше, сначала получали дополнительное практическое задание. Суть задания сильно зависела от фантазии и жестокости принимающего. На пятой лабораторной (из семи) нужно было принести консольный bmp-редактор, который поворачивал картинку на произвольный угол. Задание на оценку повыше — добавить фильтр «размытие по Гауссу». Тогда я уже прекрасно был знакомы со своим принимающим, и знал, что прописанного в задании к ЛР ему будет мало, и я решил нападать первым. Так зародилась идея вынести все фильтры в плагины и подхватывать их при загрузке редактора.

Требования к окружению

Для комфортной компиляции всех примеров требуются:

  • *NIX-подобная система (WSL тоже подходит)
  • Компилятор C, в моём случае gcc 6.3.0
  • Компилятор С++, в моём случае g++ 6.3.0
  • Система сборки, в моём случае GNU Make 4.1

Уровень 1. Учимся подхватывать библиотеки

Нашей первой целью будет незамысловатая программа, которая будет получать список *.so файлов через аргументы командной строки, искать и (в случае успеха) выполнять в них функцию такого вида:

void do_magic(void);

Для этого нам потребуется всего ничего: dlopen(3), dlsym(3), dlclose(3), dlerror(3). Помимо этого, стоит уметь работать с указателями на функции в С.

Начнём с самого простого, с кода плагина. Для первого уровня не нужно заморачиваться: выведем своеобразный «Hello World!».

hello.c

#include <stdio.h>

void do_magic(void) {
  printf("Hello from shared object!n");
}

Само «ядро» состоит лишь из вызовов функций dl* в правильном порядке.

core.c

#include <stdio.h>
#include <dlfcn.h>

void plugin_execute(const char* path) {
  /* Подгружаем динамическую библиотеку */
  void* obj = dlopen(path, RTLD_NOW);
  if(obj == NULL) {
    fprintf(stderr, "%sn", dlerror());
    return;
  }
  /* Объявляем указатель на функцию */
  void (*f)(void);
  /* Получаем адрес на фунцию do_magic() из динамической библиотеки */
  f = (void(*)(void))dlsym(obj, "do_magic");
  if(f == NULL) {
    /* Такой функции в библиотеке нет */
    fprintf(stderr, "%sn", dlerror());
  } else {
    /* Функция нашлась, выполняем */
    f();
  }
  /* Закрываем открытое */
  dlclose(obj);
}

int main(int argc, char* argv[]) {
  for(int i = 1; i < argc; i++) {
    plugin_execute(argv[i]);
  }
  return 0;
}

Сборка требует линковки с libdl для core и ключа -shared для hello.so

Команды сборки

gcc -o core core.c -ldl
gcc -o hello.so hello.c -shared

Попробуем запустить:

$ ./core ./hello.c ./hello.so /lib64/ld-linux-x86-64.so.2
./hello.c: invalid ELF header
Hello from shared object!
/lib64/ld-linux-x86-64.so.2: undefined symbol: do_magic

Отлично! Наш плагин был признан системой и функция была выполнена. В кач-ве практического задания, развивающего умения работать со строками в С и курить маны, предлагаю парсить текущий каталог и искать *.so файлы.

Дополнительный вопрос:
Что произойдёт, если в *.so файле не будет функции do_magic, а будет переменная с таким именем?

Уровень 2. Загружаем объект класса.

Сишный код — это просто. Усложним задачу. Теперь мы хотим получать не просто указатель на функцию, а целый экземпляр какого-то класса. Но так как ядро у нас не экстрасенсорное, то нам следует сделать «базу» для всех совместимых с нашей системой плагинов.

base.h

class Base {
 public:
  virtual void do_magic(void) {};
};

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

hello.cpp

#include <stdio.h>

#include "base.h"

class Hello : Base {
 public:
  void do_magic(void) {
    printf("Hello from shared object!n");
  }
};

Дело за малым: как-то получить экземпляр класса. Самый простой для нас способ: создать обёртку из сишной функции, которая будет создавать нам объект класса. Для этого стоит воспользоваться заклинанием extern "C"

hello.cpp

...
extern "C" {

void* setup_plugin(void) {
  return new Hello();
}

}

Суть заклинания

Без этого заклинания имена функций декорируются и setup_plugin выглядит как _Z12setup_pluginv. Заклинание же убеждает компилятор использовать оригинальное название, как это и происходит в обычном С.
Несложно доказать это, собрав с extern «С» и без него и проверив командой objdump -t file

Теперь адаптируем ядро для получения классов.

core.cpp

void plugin_execute(const char* path) {
  ...
  /* Изменяем указатель на функцию, которая возвращает (void*) */
  void* (*f)(void);
  /* Здесь так же изменяем приведение типа и имя функции для поиска */
  f = (void*(*)(void))dlsym(obj, "setup_plugin");
  if(f == NULL) {
    fprintf(stderr, "%sn", dlerror());
  } else {
    /* Функция нашлась, забираем у неё экземпляр нужного класса */
    Base* b = reinterpret_cast<Base*>(f());
    b->do_magic();
  }
  ...
}
...

Теперь, когда мы используем в загрузке плагинов классы С++, мы можем позволить себе не сильно заморачиваясь накрутить функционал в виде имени читаемого имени плагина, версии и всего, чего душа пожелает.

Уровень X. Скриптовый язык

При желании можно разработать собственный скриптовый язык, на котором и разрешить писать плагины. Интересный вариант, но разбираться в этой статье он явно не будет.

Заключение

Вот так не очень сложно оказалось делать плагинную систему на языках С и С++. Исходный вариант, который я сдавал на дисциплине ЯСП, имеет более сложную структуру, но так как было ограничение на используемый язык в виде С89 и делалось в ночь перед сдачей, то получилось не очень аккуратно. Тем не менее, исходный код доступен для изучения здесь: github/Firemoon777/bmp-editor.

Исходники примеров

Понравилась статья? Поделить с друзьями:
  • Как написать плагин для майнкрафт на python
  • Как написать плагин для wordpress
  • Как написать плагин для total commander
  • Как написать плагин для rust
  • Как написать плагин для notepad