Vim to the fullest: A library on which everything rests
Table of contents
- Introduction (vim_lib)
- Plugin manager without fatal flaws (vim_lib, vim_plugmanager)
- Project level and file system (vim_prj, nerdtree)
- Snippets and file templates (UltiSnips, vim_template)
- Compiling and executing anything (vim-quickrun)
- Work with Git (vim_git)
- Deploy (vim_deploy)
- Testing with xUnit (vim_unittest)
- The library on which everything rests (vim_lib)
- Other useful plugins
The main problem when writing plugins for Vim is code repetition. Unfortunately for Vim there are no libraries that solve many basic problems, which is why all plugin authors constantly step on the same rake. In this article I will try to dedicate a solution to this problem.
Foreword
To my (and possibly yours) deepest regret, I already wrote this article once, but due to my own stupidity and “features” of the Habr, I lost its most interesting chapter. In a fit of rage, I decided not to rewrite it again, since I was already very tired, therefore, dear reader, some of my thoughts will be missed. Fortunately, the lost chapter was introductory and only aimed to interest the reader. Nothing important was missed, but still insulting.
The objects
I already wrote on a habr about my attempts to implement classes in Vim. The case ended with a threefold rewriting of the decision until I came to the decision now available. It is based on the idea of objects in Perl and uses prototyping.
Let's look at a few examples:
Inheritance
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
Note that to create an instance of the class you must:
- Call the bless method that will create the object. In the example, the method does not accept parameters, which is allowed only when inheriting from the base class. If we want to inherit a class from a child class, we will need to pass an initialized instance of the parent class to this method (we will examine this later)
- Initialize the resulting class by setting its initial state
Two questions arise: why is bless needed and why is it necessary to initialize the received object if bless is already responsible for this ? Everything is simple. The bless method does a very simple operation, it creates a new dictionary and copies everything that is contained in the properties property into it , as well as non-static class methods. Two links are also set: class points to the class itself, and parent to the object of the parent class. At this point, everything is becoming more confused. If you are familiar with how C ++ is stored in objects in computer memory, then you know that to create an object of a child class, you must create an object of the parent class. For this, bless methodaccepts its only parameter. This parameter represents the finished object of the parent class that the parent reference in the child class object will point to . A link to the parent class is used to create this object. Already confused? Everything will fall into place after the following two examples:
Expand method
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 " }}}
Take a look at the implementation of the expand method . It is called on the parent class to get the descendant class. This method just creates a new dictionary, copies the parent method into it and creates the parent property (not to be confused with the property of the same name), which points to the parent class. This means that all classes in the hierarchy are connected through this property.
Bless method
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 " }}}
By understanding the implementation of the bless method, you will realize how easy it is to instantiate classes. The method creates a dictionary with references to the class and the object of the parent class, and then copies the properties and method of the class into it. After the object is created, the designer can set its state in a special way, for example, calculating the value of some properties or taking them as parameters:
Parameterized Constructor
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
Has everything become easier? But it’s still not clear why you need to pass the object of the parent class to bless , because by the implementation of this method it is clear that the object is created and installed automatically? It's all about the parameterization of the constructor. If you look again at the first line of the bless method , you will see that it uses the default constructor without parameters to create an object of the parent class. What if the parent constructor is parameterized? In this case, we must create the object of the parent class with our own hands and give it bless :
Parent class object
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
I hope it’s clear now. Let's go further:
Instantiation
let s:List = vim_lib#base#List#
let s:objList = s:List.new() " Инстанциируем класс
Since all classes of the library (as well as plugins) are stored (usually, but not necessarily) in autoload , they are accessed through the scope with automatic loading of the necessary files. In order not to constantly spell these long names, an alias is used by simply assigning the class (after all, the class is an object) to a variable, which is then used in the script. To instantiate a class, the familiar new method is used .
The base class also provides the following methods:
- typeof - the method takes a class and checks if the called class is a child of the parameter
- mix - the method adds all the methods and properties of the parameter class to the called class, implementing impurities
In general, the object model in vim_lib ends here; everything else is specific solutions to various problems.
Library structure
The entire library is located in the autoload directory . This allows you to load parts of it as needed and use namespaces. The library consists of the following packages:
- base - the basic library components, such as dictionaries, arrays, stacks, files, and so on
- sys - system components that represent the logic of the editor, such as buffers, plugins, the editor itself
- view - widgets
base
The base package contains simple classes that represent low-level components. This includes object representations of basic structures, such as dictionaries, lists, and stacks, as well as auxiliary classes for working with the file system, event model, and unit tests. If I look at each of these classes, the article will turn into a book, so I will limit myself to a cursory review.
sys
This package includes classes representing the components of the editor and some of its logic. The Buffer, System and Conten classes represent the editor itself, as well as the elements with which it works (buffers and text in them), and Plugin and Autoload determine the model of plug-ins and initialization of the editor as a whole.
view
The view package is still very small, as it contains a single class that represents a simple widget with stacking buffers overlapping each other. I selected this package for implementing non-standard solutions in the editor interface.
Editor Initialization Model
The vim_lib # sys # Autoload class deserves special attention, since it defines (but does not impose) the basic logic for initializing the editor and loading plugins. This is the only library class that is not inherited from the base class Object, as this was not necessary. By the way, the library does not require the use of the proposed object model, it just offers one of the proven implementations that you can use. But let's not go far from the topic. The Autoload class monitors which directory will be used in each of the initialization steps to load editor components. These directories are called levels and so far three main ones have been distinguished:
- System-wide - scripts in this directory apply to all users
- User - scripts that are used for all projects of a specific user
- Project - scripts for a specific project
To use the proposed model, just add the following code to .vimrc :
Autoload Connection
filetype off
set rtp=~/.vim/bundle/vim_lib
call vim_lib#sys#Autoload#init('~/.vim', 'bundle')
" Плагины
filetype indent plugin on
The init method determines the root directory for the current level and the name of the directory that stores the plugins. You can read more about this in one of my previous articles .
Plugin model
The vim_lib library also offers a unified plug-in model that uses the vim_lib # sys # Plugin class as the base class. This class defines many standard methods, and also implements the logic of connecting plug-ins with checking conditions and dependencies.
A plugin using this model has a structure familiar to all plugin writers:
- In the plugin directory, there is a plug-in file containing a child with respect to vim_lib # sys # Plugin, which is responsible for initializing the plug-in. This class (or rather its object) is responsible for the plug-in options, its initialization, as well as for adding commands and menu items
- The autoload directory contains the plugin interface files. These files contain functions that are, as it were, plugin methods. Since the autoload directory is used , all functions are loaded as needed
- In the doc , test , syntax and so on directories , other plug-in files are located (according to the old scheme)
Consider a few examples:
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() " Регистрация плагина
This simple example shows that the plugin is created as an object of the vim_lib # sys # Plugin class, which is populated with methods and properties, and then registered with the system. Since the scripts in the plugin directory are executed when the editor is initialized, this file will be executed every time Vim starts, which will allow you to create and register the plugin object.
autoload / myPlug.vim
function! myPlug#run()
echo 'Hello world'
endfunction
The plugin file in the autoload directory includes the public functions of the plugin, which are used by the commands and menu items of the plugin. Other files used by the plugin may also be located in this directory, but the autoload / plugin_name.vim file is the main one. These functions are called when working with the plugin.
To connect the plug-in to the editor, just add the following entry to your .vimrc :
Plugin connection
filetype off
set rtp=~/.vim/bundle/vim_lib
call vim_lib#sys#Autoload#init('~/.vim', 'bundle')
Plugin 'MyPlug', {
\ 'options': { опции плагина },
\ 'map': { горячие клавиши },
\ и так далее
\}
filetype indent plugin on
When declaring a plugin, you can specify its options, hot keys, override commands and menu items.
Unit tests
The library includes many unit tests thanks to the vim_lib # base # Test class, which implements the basic logic of unit testing using the proposed object model library.
Dict class test
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()
Bye bye
The article turned out two times shorter than what I was counting on. After the habr decided to log me out and reload the page with the article, deleting half of the operating time (I don’t know what it was, maybe a bug), I gathered my strength and finished writing the article, albeit in an abridged version. A lot remains behind the scenes, because the article may seem incomprehensible and superficial. So that this does not happen again, I decided to move away from using the Habr as the main platform for the vim_lib project, and use third-party services or my own developments that exclude such an annoying data loss.
If you find something incomprehensible in this article, ask, I will try to convey the features of the library in a simple and understandable language.