Vim по полной: Библиотека, на которой все держится


  1. Введение (vim_lib)
  2. Менеджер плагинов без фатальных недостатков (vim_lib, vim_plugmanager)
  3. Уровень проекта и файловая система (vim_prj, nerdtree)
  4. Snippets и шаблоны файлов (UltiSnips, vim_template)
  5. Компиляция и выполнение чего угодно (vim-quickrun)
  6. Работа с Git (vim_git)
  7. Деплой (vim_deploy)
  8. Тестирование с помощью xUnit (vim_unittest)
  9. Библиотека, на которой все держится (vim_lib)
  10. Другие полезные плагины


Основной проблемой при написании плагинов под Vim, является повторение кода. К сожалению для Vim нет библиотек, решающих множество базовых задач, отчего все авторы плагинов постоянно наступают на одни и те же грабли. В этой статье я постараюсь освятить решение этой проблемы.
К моему (и возможно вашему) глубочайшему сожалению, я уже одни раз писал эту статью, но по собственной глупости и «особенности» хабра, я потерял самую интересную ее главу. В порыве ярости я решил не переписывать ее вновь, так как и так очень устал, потому, дорогой читатель, некоторая часть моей мысли будет упущено. К счастью, потерянная глава была вводной и лишь преследовала цель заинтересовать читателя. Ничего важного упущено не было, но все равно обидно.
Я уже писал на хабре о моих попытках реализовать классы в Vim. Дело закончилось трехкратным переписыванием решения до тех пор, пока я не пришел к имеющемуся сейчас решению. Оно основано на идеи объектов в Perl и использует прототипирование.

Рассмотрим несколько примеров:

Наследование
let s:Object = g:vim_lib#base#Object#

let s:Class = s:Object.expand() " Наследуем от базового класса
" Конструктор
function! s:Class.new(...)
  let l:obj = self.bless() " Создаем объект (кто узнал метод bless?)
  let l:obj.values = (exists('a:1') && type(a:1) == 3)? a:1 : [] " Инициализируем объект
  return l:obj " Возвращаем объект
endfunction

" Другие методы класса
function! s:Class.length() " {{{
  return len(self.values)
endfunction " }}}

" ...

" Делаем класс общедоступным
let g:vim_lib#base#List# = s:Class



Обратите внимание, что для создания экземпляра класса необходимо:

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


Возникает два вопроса: зачем нужен bless и почему необходимо инициализировать полученный объект, если за это уже отвечает bless? Все просто. Метод bless делает очень простую операцию, он создает новый словарь и копирует в него все что содержится в свойстве properties, а так же не статичные методы класса. Так же устанавливается две ссылки: class указывает на сам класс, а parent на объект родительского класса. На этом моменте все становится более запутанно. Если вы знакомы с тем, как хранятся в объекты в памяти компьютера языком C++, то знаете, что для создания объекта дочернего класса, необходимо создать объект родительского класса. Именно для этого метод bless принимает свой единственный параметр. Этот параметр представляет готовый объект родительского класса, на который будет указывать ссылка parent в объекте дочернего класса. Для создания этого объекта используется ссылка на родительский класс. Уже запутались? Все встанет на места после двух следующих примеров:

Метод expand
function! s:Class.expand() " {{{
  let l:child = {'parent': self}
  " Перенос ссылок на методы класса в подкласс. {{{
  let l:child.expand = self.expand
  let l:child.mix = self.mix
  let l:child.new = self.new
  let l:child.bless = self.bless
  let l:child.typeof = self.typeof
  " }}}
  return l:child
endfunction " }}}



Взгляните на реализацию метода expand. Он вызывается у родительского класса для получения класса-потомка. Этот метод всего лишь создает новый словарь, копирует в него метода родителя и создает свойство parent (не путать с одноименным свойством объектов), которое указывает на класс-родитель. Это означает, что все классы в иерархии связаны через это свойство.

Метод bless
function! s:Class.bless(...) " {{{
  let l:obj = {'class': self, 'parent': (exists('a:1'))? a:1 : self.parent.new()}
  " Перенос частных методов из класса в объект. {{{ 
  for l:p in keys(self)
    if type(self[l:p]) == 2 && index(['expand', 'mix', 'bless', 'new', 'typeof'], l:p) == -1 && l:p[0:1] != '__'
      let l:obj[l:p] = self[l:p]
    endif
  endfor
  " }}}
  " Перенос свойств из класса в объект. {{{
  if has_key(self, 'properties')
    for [l:k, l:v] in items(self.properties)
      let l:obj[l:k] = deepcopy(l:v)
    endfor
  endif
  " }}}
  return l:obj
endfunction " }}}



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

Параметризованный конструктор
function! s:Class.new(x, y)
  let l:obj = self.bless()
  let l:obj.x = a:x
  let l:obj.y = a:y
  return l:obj
endfunction



Все стало проще? Но все еще не понятно, зачем нужно передавать в bless объект родительского класса, ведь по реализации этого метода видно, что объект создается и устанавливается автоматически? Все дело в параметризации конструктора. Если вы еще раз посмотрите на первую строчку метода bless, то увидите что он использует по умолчанию конструктор без параметров для создания объекта родительского класса. Что делать, если конструктор у родителя параметризован? В это случае мы должны собственноручно создать объект родительского класса и предать его bless:

Объект родительского класса
function! s:Class.new(x, y)
  let l:parent = self.parent.new(a:x) " Получаем объект родительского класса с помощью его конструктора
  let l:obj = self.bless(l:parent) " Получаем заготовку объекта текущего класса
  let l:obj.y = a:y
  return l:obj
endfunction



Надеюсь теперь понятно. Пойдем дальше:

Инстанциация
let s:List = vim_lib#base#List#
let s:objList = s:List.new() " Инстанциируем класс



Так как все классы библиотеки (а так же плагинов) хранятся (как правило, но не обязательно) в autoload, доступ к ним осуществляется через области видимости с автоматической загрузкой нужных файлов. Чтобы не прописывать постоянно эти длинные имена, используется псевдоним, путем простого присваивания класса (ведь класс это объект) переменной, которая потом используется в скрипте. Для инстанциации класса, используется уже знакомый метод new.

Базовый класс так же предоставляет следующие методы:

  • typeof — метод принимает класс и проверяет, является ли вызываемый класс дочерним по отношению к параметру
  • mix — метод добавляет все методы и свойства класса-параметра в вызываемый класс, реализуя примеси


В общем на этом модель объектов в vim_lib заканчивается, все остальное это конкретные решения различных задач.
Вся библиотека располагается каталоге autoload. Это позволяет загружать ее части по мере надобности и использовать пространства имен. Библиотек состоит из следующих пакетов:

  • base — базовые компоненты библиотеки, такие как словари, массивы, стеки, файлы и так далее
  • sys — системные компоненты, представляющие логику редактора, такие как буферы, плагины, сам редактор
  • view — виджеты


base


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

sys


Этот пакет включает классы, представляющие компоненты редактора и некоторую его логику. Классы Buffer, System и Conten представляют сам редактор, а так же элементы, с которыми он работает (буферы и текст в них), а Plugin и Autoload определяют модель плагинов и инициализации редактора в целом.

view


Пакет view пока очень мал, так как содержит единственный класс, который представляет простой виджет с перекрывающими друг друга буферами в виде стека. Этот пакет я выделил для реализации нестандартных решений в интерфейсе редактора.
Класс vim_lib#sys#Autoload заслуживает особое внимание, так как он определяет (но не навязывает) базовую логику инициализации редактора и загрузки плагинов. Это единственный класс библиотеки, который не унаследован от базового класса Object, так как в этом не было необходимости. Кстати, библиотека не требует использовать предлагаемую ей объектную модель, она всего лишь предлагает одну из проверенных реализации, которой можно воспользоваться. Но не будем отходить далеко от тему. Класс Autoload следит за тем, какой каталог будет использован в каждом из этапов инициализации для загрузки компонентов редактора. Эти каталоги называются уровнями и пока выделено три основных:

  • Общесистемный — скрипты этого каталога распространяются на всех пользователей
  • Пользовательский — скрипты, которые применяются для всех проектов конкретного пользователя
  • Проектный — скрипты для конкретного проекта


Для использования предложенной модели достаточно добавить в .vimrc следующий код:

Подключение Autoload
filetype off 
set rtp=~/.vim/bundle/vim_lib
call vim_lib#sys#Autoload#init('~/.vim', 'bundle')
" Плагины
filetype indent plugin on



Метод init определяет корневой каталог для текущего уровня и имя каталога, хранящего плагины. Подробнее об этом можно прочитать в одной из моих предыдущих статей.
Библиотека vim_lib так же предлагает унифицированную модель плагинов, использующую класс vim_lib#sys#Plugin как базовый. Этот класс определяет множество стандартных методов, а так же реализует логику подключения плагинов с проверкой условий и зависимостей.

Плагин, использующий эту модель, имеет знакомую всем плагинописцам структуру:

  • В каталоге plugin располагается файл плагина, содержащий дочерний по отношению к vim_lib#sys#Plugin, отвечающий за инициализацию плагина. Этот класс (а точнее его объект) отвечает за опции плагина, его инициализацию, а так же за добавление команд и пунктов меню
  • В каталоге autoload располагаются файлы интерфейса плагина. Эти файлы содержат функции, являющиеся как бы методами плагина. Так как используется каталог autoload, все функции загружаются по мере надобности
  • В каталогах doc, test, syntax и так далее, располагаются другие файлы плагина (по старой схеме)

Рассмотрим несколько примров:

plugin/myPlug.vim
let s:Plugin = vim_lib#sys#Plugin#

let s:p = s:Plugin.new('myPlug', '1', {'plugins': ['vim_lib']}) " Создание плагина, определяя его имя, версию и зависимости
let s:p.x = 1 " Опции плагина
" Метод инициализации плагина, вызываемый при старте Vim
function! s:p.run()
  ...
endfunction

" Команды плагина
call s:p.comm('MyCommand', 'run()')

" Пункты меню
call s:p.menu('Run', 'run', 1)

call s:p.reg() " Регистрация плагина



В этом простом примере видно, что плагин создается как объект класса vim_lib#sys#Plugin, который заполняется методами и свойствами, а затем регистрируется в системе. Так как скрипты в каталоге plugin выполняются при инициализации редактора, этот файл будет выполнен при каждом старте Vim, что позволит создать и зарегистрировать объект плагина.

autoload/myPlug.vim
function! myPlug#run()
  echo 'Hello world'
endfunction



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

Для подключения плагина к редактору достаточно добавить следующую запись в ваш .vimrc:

Подключение плагина
filetype off 
set rtp=~/.vim/bundle/vim_lib
call vim_lib#sys#Autoload#init('~/.vim', 'bundle')
Plugin 'MyPlug', {
\  'options': { опции плагина },
\  'map': { горячие клавиши },
\  и так далее
\}
filetype indent plugin on



При объявлении плагина можно указать его опции, горячие клавиши, переопределить команды и пункты меню.
Библиотека включает множество модульных тестов благодаря классу vim_lib#base#Test, который реализует основную логику модульного тестирования с использованием предложенной библиотекой объектной модели.

Тест класса Dict
let s:Dict = vim_lib#base#Dict#

let s:Test = vim_lib#base#Test#.expand()

" new {{{
"" {{{
" Должен создавать пустой словарь.
" @covers vim_lib#base#Dict#.new
"" }}}
function s:Test.testNew_createEmptyDict() " {{{
  let l:obj = s:Dict.new()
  call self.assertEquals(l:obj.length(), 0)
endfunction " }}}

"" {{{
" Должен использовать хэш в качестве начальных данных.
" @covers vim_lib#base#Dict#.new
"" }}}
function s:Test.testNew_wrapHash() " {{{
  let l:obj = s:Dict.new({'a': 1, 'b': 2, 'c': 3})
  call self.assertEquals(l:obj.length(), 3)
  call self.assertEquals(l:obj.item('a'), 1)
endfunction " }}}

"" {{{
" Должен использовать массив в качестве начальных данных.
" @covers vim_lib#base#Dict#.new
"" }}}
function s:Test.testNew_wrapArray() " {{{
  let l:obj = s:Dict.new([['a', 1], ['b', 2], ['c', 3]])
  call self.assertEquals(l:obj.length(), 3)
  call self.assertEquals(l:obj.item('a'), 1)
endfunction " }}}
" }}}
" item {{{
"" {{{
" Должен возвращать значение элемента словаря по ключу.
" @covers vim_lib#base#Dict#.item
"" }}}
function s:Test.testItem_getValue() " {{{
  let l:obj = s:Dict.new()
  call l:obj.item('a', 1)
  call l:obj.item('b', 2)
  call self.assertEquals(l:obj.item('a'), 1)
  call self.assertEquals(l:obj.item('b'), 2)
endfunction " }}}

"" {{{
" Должен выбрасывать исключение, если элемент с заданым ключем отсутствует.
" @covers vim_lib#base#Dict#.item
"" }}}
function s:Test.testItem_throwExceptionGet() " {{{
  let l:obj = s:Dict.new()
  try
    call l:obj.item('a')
    call self.fail('testItem_throwException', 'Expected exception  is not thrown.')
  catch /IndexOutOfRangeException:.*/
  endtry
endfunction " }}}

"" {{{
" Должен устанавливать значение элементу словаря.
" @covers vim_lib#base#Dict#.item
"" }}}
function s:Test.testItem_setValue() " {{{
  let l:obj = s:Dict.new()
  call l:obj.item('a', 1)
  call self.assertEquals(l:obj.item('a'), 1)
endfunction " }}}
" }}}
" keys, vals, items {{{
"" {{{
" Должен возвращать массив ключей словаря.
" @covers vim_lib#base#Dict#.keys
"" }}}
function s:Test.testKeys() " {{{
  let l:obj = s:Dict.new({'a': 1, 'b': 2, 'c': 3})
  call self.assertEquals(l:obj.keys(), ['a', 'b', 'c'])
endfunction " }}}

"" {{{
" Должен возвращать массив значений словаря.
" @covers vim_lib#base#Dict#.vals
"" }}}
function s:Test.testValues() " {{{
  let l:obj = s:Dict.new({'a': 1, 'b': 2, 'c': 3})
  call self.assertEquals(l:obj.vals(), [1, 2, 3])
endfunction " }}}

"" {{{
" Должен возвращать массив элементов словаря.
" @covers vim_lib#base#Dict#.items
"" }}}
function s:Test.testItems() " {{{
  let l:obj = s:Dict.new({'a': 1, 'b': 2, 'c': 3})
  call self.assertEquals(l:obj.items(), [['a', 1], ['b', 2], ['c', 3]])
endfunction " }}}
" }}}

let g:vim_lib#base#tests#TestDict# = s:Test
call s:Test.run()



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

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

© Habrahabr.ru