📜 ⬆️ ⬇️

Vim in full: The library on which everything rests

Table of contents


  1. Introduction (vim_lib)
  2. Plugin Manager without fatal flaws (vim_lib, vim_plugmanager)
  3. Project level and file system (vim_prj, nerdtree)
  4. Snippets and File Templates (UltiSnips, vim_template)
  5. Compiling and doing anything (vim-quickrun)
  6. Work with Git (vim_git)
  7. Deploy (vim_deploy)
  8. Testing with xUnit (vim_unittest)
  9. The library on which everything is kept (vim_lib)
  10. Other useful plugins

The main problem when writing plugins for Vim is code repetition. Unfortunately, there are no libraries for Vim that solve a lot of basic tasks, which is why all authors of plugins constantly tread on the same rake. In this article I will try to sanctify the solution to this problem.

Foreword


To my (and maybe your) 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, as I was so tired, because, dear reader, some of my thoughts will be lost. Fortunately, the lost chapter was an introductory one and only pursued the goal to interest the reader. Nothing important was missed, but still offensive.

Objects


I already wrote in Habré about my attempts to implement classes in Vim. The case ended with a three-fold rewriting of the decision until I came to the solution now available. It is based on the ideas of objects in Perl and uses prototyping.

Consider 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 


Please note that to create an instance of a class you need:

Two questions arise: why bless is needed and why is it necessary to initialize the received object if bless is already responsible for it? It's simple. The bless method makes a very simple operation, it creates a new dictionary and copies everything that is contained in the properties property, as well as non-static class methods. There are also two links: the class points to the class itself, and the parent points to an object of the parent class. At this point, everything becomes more confusing. If you are familiar with how objects are stored in memory in a computer using C ++, then you know that to create an object of a child class, you must create an object of the parent class. For this, the bless method takes its only parameter. This parameter represents the finished object of the parent class, to which the parent reference in the child class object will point. A reference 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 only creates a new dictionary, copies the parent method into it, and creates the parent property (not to be confused with the object property of the same name), which points to the parent class. This means that all classes in the hierarchy are linked 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 " }}} 


After examining the implementation of the bless method, you will understand how simple class instances are created. The method creates a dictionary with references to the class and object of the parent class, and then copies into it the properties and methods of the class. After an object is created, the designer can set its state in a special way, for example, by 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 things become easier? But it is still not clear why it is necessary to transfer an object of the parent class to bless , because the implementation of this method shows 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 no-parameter constructor to create an object of the parent class. What if the parent constructor is parameterized? In this case, we have to personally create an object of the parent class and deliver it to 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 now is clear. 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 plug-ins) are stored (as a rule, but not necessarily) in autoload , they are accessed through scopes with automatic loading of the necessary files. In order not to permanently prescribe these long names, an alias is used by simply assigning a class (after all, a class is an object) to a variable, which is then used in the script. To instantiate a class, the already familiar new method is used.
')
The base class also provides the following methods:

In general, the object model in vim_lib ends there, the rest is concrete 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 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 supporting classes for working with the file system, event model, and unit tests. If I examine each of these classes, the article will turn into a book, therefore I will limit myself to a cursory review.

sys


This package includes classes that represent 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 define the plug-in model and the editor's initialization as a whole.

view


The view package is still very small, as it contains a single class that represents a simple widget with overlapping stack-based buffers. 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 of initializing the editor and loading plug-ins. This is the only library class that is not inherited from the base Object class, since this was not necessary. By the way, the library does not require using the object model offered to it, it only offers one of the proven implementations that you can use. But let's not go far from the topic. The Autoload class keeps track of which directory will be used in each of the initialization steps to load the components of the editor. These directories are called levels and while three main ones are highlighted:

To use the proposed model, simply 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 plugins. Read more about this in one of my previous articles .

Plugin model


The vim_lib library also offers a unified plugin model using the vim_lib # sys # Plugin class as the base. This class defines a set of standard methods, as well as implements the plug-in connection logic with checking conditions and dependencies.

A plugin using this model has a structure familiar to all plug-in writers:


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:px = 1 "   "   ,    Vim function! s:p.run() ... endfunction "   call s:p.comm('MyCommand', 'run()') "   call s:p.menu('Run', 'run', 1) call s:p.reg() "   


In this simple example, you can see that the plugin is created as an object of class vim_lib # sys # Plugin, which is filled with methods and properties, and then registered in the system. Since the scripts in the plugin directory are executed when the editor is initialized, this file will be executed every time Vim is started, which allows you to create and register a plug-in object.
autoload / myPlug.vim
 function! myPlug#run() echo 'Hello world' endfunction 


The plugin file in the autoload directory includes the public plugin functions that are used by the commands and menu items of the plugin. This directory may also contain other files used by the plugin, but the autoload / Plugin name.vim file is the main one. These functions are called when working with the plugin.

To connect the plugin to the editor, simply add the following entry to your .vimrc :
Plug-in 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 you declare a plug-in, 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 <IndexOutOfRangeException> 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 all


The article turned out two times shorter than what I expected. After Habr decided to log me out and reload the page with the article, deleting half the work (I don’t know what it was, maybe a bug), I gathered my strength and finished the article, albeit in an abbreviated version. Behind the scenes, a lot remains, because the article may seem incomprehensible and superficial. To prevent this from happening 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 workouts that preclude 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.

Source: https://habr.com/ru/post/264351/


All Articles