Статьи

Как создать плагин Sublime Text 2

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

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


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

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

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

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

В процессе написания плагина будет необходима ссылка на Sublime Text 2 API .

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

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


Sublime поставляется с функциональностью, которая генерирует скелет кода Python, необходимый для написания простого плагина. Выберите пункт меню « Инструменты»> «Новый плагин…» , и с этим образцом будет открыт новый буфер.

1
2
3
4
5
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 .

01
02
03
04
05
06
07
08
09
10
Packages/
— OCaml/
— Perl/
— PHP/
— Prefixr/
— Python/
— R/
— Rails/

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

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

1
view.run_command(‘example’)

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


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

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

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

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

1
class ExampleCommand(sublime_plugin.TextCommand):

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

1
view.run_command(‘example’)

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

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

1
class PrefixrCommand(sublime_plugin.TextCommand):

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

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

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

1
2
3
4
5
braces = False
sels = self.view.sel()
for sel in sels:
    if self.view.substr(sel).find(‘{‘) != -1:
        braces = True

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

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

1
2
3
4
5
6
7
8
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-файле исходного кода.


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

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

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


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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

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

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

1
2
3
import urllib
import urllib2
import threading

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

1
2
3
4
5
6
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-файле исходного кода.


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

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

1
self.view.sel().clear()

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

1
edit = self.view.begin_edit(‘prefixr’)

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

1
self.handle_threads(edit, threads, braces)

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

01
02
03
04
05
06
07
08
09
10
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() который мы скоро напишем.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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() , но мы не сможем указать параметры.

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

1
2
3
4
5
6
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-файле исходного кода.


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

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

1
2
3
4
5
6
7
8
9
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, длина этого блока увеличится. Смещение гарантирует, что мы заменяем правильный контент для последующих выделений, так как все позиции текста смещаются при каждой замене.

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

1
2
3
4
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-кода, который мы вставили, а затем возвращаем скорректированное смещение, чтобы использовать его для дальнейшего выбора.

1
2
3
4
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-файле исходного кода.


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

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

1
2
3
4
5
6
7
8
def normalize_line_endings(self, string):
   string = string.replace(‘\r\n’, ‘\n’).replace(‘\r’, ‘\n’)
   line_endings = self.view.settings().get(‘default_line_ending’)
   if line_endings == ‘windows’:
       string = string.replace(‘\n’, ‘\r\n’)
   elif line_endings == ‘mac’:
       string = string.replace(‘\n’, ‘\r’)
   return string

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

1
2
3
4
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. Это делается путем поиска пробелов в начале выделения.

1
2
3
4
5
6
(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 до исходного уровня, используя табуляции или пробелы в зависимости от текущих настроек редактора.

01
02
03
04
05
06
07
08
09
10
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 с префиксом точно соответствовал оригиналу.

1
2
3
4
5
6
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() мы использовали модуль регулярного выражения Python (re), поэтому нам нужно добавить его в список импорта в верхней части скрипта.

1
import re

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


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

1
2
3
4
5
6
Prefixr/
— Default (Linux).sublime-keymap
— Default (OSX).sublime-keymap
— Default (Windows).sublime-keymap
— Prefixr.py

.sublime-keymap содержат массив JSON, который содержит объекты JSON для указания привязок клавиш. Объекты JSON должны содержать keys и ключ command , а также могут содержать ключ args если команде требуются аргументы. Самое сложное в выборе привязки ключа — убедиться, что привязка ключа еще не используется. Это можно сделать, перейдя в меню « Настройки»> «Привязки клавиш» — пункт «По умолчанию» и выполнив поиск клавиш, которые вы хотите использовать. Найдя подходящую неиспользуемую привязку, добавьте ее в свои .sublime-keymap .

1
2
3
4
5
[
    {
        «keys»: [«ctrl+alt+x»], «command»: «prefixr»
    }
]

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


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

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

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

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

01
02
03
04
05
06
07
08
09
10
[
{
    «id»: «edit»,
    «children»:
    [
        {«id»: «wrap»},
        { «command»: «prefixr» }
    ]
}
]

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

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


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

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

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

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

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


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