Ve druhé části článku o novinkách, které můžeme nalézt v osmé verzi populárního textového editoru Vim, se seznámíme s některými změnami provedenými ve skriptovacím engine Vimu. S těmito změnami a vylepšeními se přímo setkají většinou pouze vývojáři pluginů, ovšem těžit z nich (alespoň nepřímo) mohou i všichni ostatní uživatelé Vimu, protože implementované změny a rozšíření (anonymní funkce, časovače, unikátní čísla oken atd.) umožňují tvorbu složitějších pluginů pracujících asynchronně s operacemi prováděnými uživatelem.

Obsah

1. Textový editor Vim 8 - změny ve skriptovacím engine Vimu

2. Unikátní čísla oken

3. Příklad číslo 1: přečtení pořadí okna (kód kompatibilní s Vimem 7)

4. Příklad číslo 2: přečtení ID oken (kód pro Vim 8)

5. Anonymní funkce

6. Reference na funkce

7. Příklad číslo 3: použití reference na funkci společně s funkcí vyššího řádu map

8. Příklad číslo 4: použití anonymních funkcí s funkcí vyššího řádu map

9. Příklad číslo 5: funkce map neměnící vstupní sekvenci

10. Příklad číslo 6: použití anonymních funkcí s funkcí vyššího řádu filter

11. Časovače

12. Příklad číslo 7: vytvoření a použití několika časovačů

13. Repositář s demonstračními příklady

14. Odkazy na Internetu

1. Textový editor Vim 8 - změny ve skriptovacím engine Vimu

V úvodním článku o osmé verzi populárního textového editoru Vim jsme si nejprve stručně popsali historii tohoto programu a následně jsme se seznámili s některými novinkami, s nimiž se setkají prakticky všichni uživatelé Vimu, nezávisle na tom, zda se jedná o uživatele tohoto textového editoru nebo o vývojáře rozšiřujících modulů (pluginů). Dnes si popíšeme některé změny, které byly implementovány ve skriptovacím engine Vimu; tyto změny jsou tedy použitelné ve VimScriptu. Tyto změny sice mohou na první pohled vypadat nenápadně, ve skutečnosti však poměrně významným způsobem rozšiřují možnosti pluginů. Dá se to přirovnat k situaci webových aplikací před a po implementaci objektu XMLHttpRequest do JavaScriptových enginů v browserech, protože i skriptovací engine Vimu nyní umožňuje tvorbu asynchronního kódu, registraci funkcí zavolaných až po určitém časovém intervalu s využitím časovačů (timers) apod. Všechny popisované novinky budou ukázány na jednoduchých skriptech.

Obrázek 1: Všechny změny a vylepšení Vimu 8 jsou samozřejmě popsány v interním systému nápovědy (helpu).

2. Unikátní čísla oken

Nejprve si krátce zopakujme terminologii používanou ve Vimu. V tomto textovém editoru se pracuje s takzvanými buffery (což je typicky obsah textového souboru načtený do paměti), okny (ta představují „pohledy“ do bufferu, přičemž těchto „pohledů“ může existovat libovolně mnoho) a záložkami/taby, na nichž jsou okna zobrazena. V předchozích verzích Vimu bylo každé okno na záložce reprezentované svým indexem, přičemž index 1 byl přiřazen oknu umístěnému do levého horního rohu a indexy se zvyšovaly směrem doprava a potom dolů. Pokud byla například plocha editoru rozdělena na čtyři stejně velká čtvercová okna, byly jim jejich indexy přiřazeny následovně:

+-----+-----+
|  1  |  2  |
+-----+-----+
|  3  |  4  |
+-----+-----+

Problém nastane v případě, kdy nějaký plugin potřebuje pracovat s určitým oknem. Představme se například klasický plugin NETRW, který musí obsluhovat své vlastní okno se seznamem souborů a adresářů. Pokud uživatel nějaké okno zavře či ho pouze přesune, dojde ke změně indexů oken, takže plugin ztratí vazbu na okno, se kterým potřebuje pracovat:

Obrázek 2: Speciální okno používané pluginem NETRW, které se otevře příkazem :Explore či :Vexplore.

Aby bylo možné každé okno jednoznačně identifikovat po celou dobu jeho existence, byl do Vimu 8 přidán koncept takzvaných unikátních identifikátorů oken. Každému oknu jsou tedy přiřazena dvě celá čísla – index (ten se může měnit, přičemž vždy existují okna s indexy 1..počet_oken) a identifikátor okna (ten se nikdy nemění a není ani použit znovu, tj. pokud se okno zavře a vytvoří se nové, získá odlišné ID). Současně byly do Vimu přidány další funkce pro práci s identifikátory oken, zejména pak funkce win_getid() (převod indexu okna na jeho ID), win_gotoid() (přechod na okno se zadaným identifikátorem), win_id2win (převod ID okna na jeho index) a win_id2tabwin() (dtto, ale vrátí se i identifikátor záložky). Ve chvíli, kdy plugin otevře nové okno, může si zapamatovat jeho ID a potom kdykoli použít funkci win_gotoid() pro přechod do tohoto okna.

Obrázek 3: Popis funkce win_getid(), která byla přidána do Vimu 8.

3. Příklad číslo 1: přečtení pořadí okna (kód kompatibilní s Vimem 7)

Nejprve si ukážeme, jak se s okny pracovalo v předchozích verzích Vimu. V následujícím skriptu jsou definovány dvě funkce nazvané SplitCurrentWindow a PrintWindowNumbers (názvy funkcí ve VimScriptu začínají velkým písmenem). Ve funkci pojmenované SplitCurrentWindow dojde k několikerému horizontálnímu a vertikálnímu rozdělení okna editoru. Povšimněte si, že v programové smyčce rozdělíme jedno okno desetkrát, ovšem ihned po rozdělení nově vzniklé okno zase zavřeme. Pokud je tato funkce spuštěna ve chvíli, kdy editor obsahuje jen jediné okno, bude výsledek vypadat následovně:

Obrázek 4: Takto vypadá plocha editoru po dokončení prvního demonstračního skriptu.

Ve druhé funkci pojmenované PrintWindowNumbers se naplňuje seznam windowNumbers indexy existujících oken. Využíváme zde funkci windo, která postupně spustí specifikovaný kód pro všechna existující okna. Povšimněte si, jak je možné seznam naplnit – používá se zde funkce winnr() vracející index aktuálního okna:

windo call add(windowNumbers, winnr()) 

Úplný programový kód tohoto příkladu vypadá následovně:

" ---------------------------------------------
" Vim8 example script #1 - this script creates
" multiple windows and then prints its numbers
"
" How to use it:
" 1) start new Vim session
" 2) open this script in it
" 3) call :source %
" ---------------------------------------------

function! SplitCurrentWindow()
    split

    " create and close ten windows
    for i in range(10)
        split  " split new window
        close  " and close it immediatelly
    endfor

    vsplit
    split
    vsplit
endfunction

function! PrintWindowNumbers()
    let windowNumbers = []
    windo call add(windowNumbers, winnr()) 
    for windowNumber in windowNumbers
        echo windowNumber
    endfor
endfunction

call SplitCurrentWindow()
call PrintWindowNumbers()

Po spuštění by se měly vypsat indexy oken od 1 do 5 (nezávisle na tom, že vlastně mezitím vzniklo deset dalších oken):

Obrázek 5: Indexy oken vypsané po spuštění prvního demonstračního skriptu.

4. Příklad číslo 2: přečtení ID oken (kód pro Vim 8)

Druhý demonstrační skript již využívá možností Vimu 8 a nebude ho tedy možné spustit v předchozích verzích tohoto editoru. Do skriptu byla přidána funkce PrintWindowIDs, v níž se voláním win_getid(index_okna) zjišťuje unikátní identifikátor každého nalezeného okna:

function! PrintWindowIDs()
    let windowIDs = []
    windo call add(windowIDs, win_getid(winnr())) 
    for windowID in windowIDs
        echo windowID
    endfor
endfunction

Obrázek 6: Takto vypadá plocha editoru po dokončení druhého demonstračního skriptu.

Zatímco indexy oken zůstávají stejné (1..5), budou se jejich identifikátory lišit, což je patrné ze screenshotu zobrazeného na sedmém obrázku. Povšimněte si, že ID obsahují skok z hodnoty 1001 až na hodnotu 1012, což je ale pochopitelné, protože mezitím vzniklo a ihned poté zaniklo deset dalších oken:

Obrázek 7: Indexy a identifikátory oken vypsané po spuštění druhého demonstračního skriptu.

Úplný programový kód druhého příkladu (skriptu) vypadá následovně:

" ---------------------------------------------
" Vim8 example script #2 - this script creates
" multiple windows and then prints its numbers
" and unique IDs
"
" How to use it:
" 1) start new Vim session
" 2) open this script in it
" 3) call :source %
" ---------------------------------------------

function! SplitCurrentWindow()
    split

    " create and close ten windows
    for i in range(10)
        split  " split new window
        close  " and close it immediatelly
    endfor

    vsplit
    split
    vsplit
endfunction

function! PrintWindowNumbers()
    let windowNumbers = []
    windo call add(windowNumbers, winnr()) 
    for windowNumber in windowNumbers
        echo windowNumber
    endfor
endfunction

function! PrintWindowIDs()
    let windowIDs = []
    windo call add(windowIDs, win_getid(winnr())) 
    for windowID in windowIDs
        echo windowID
    endfor
endfunction

call SplitCurrentWindow()
call PrintWindowNumbers()
call PrintWindowIDs()

5. Anonymní funkce

Další novinkou, kterou nalezneme ve skriptovacím jazyku VimScript, jsou anonymní funkce (někdy se setkáme s možná přiléhavějším názvem lambdy, což je název vycházející z lambda kalkulu). Anonymní funkce je možné použít ve všech místech programového kódu, kde jsou očekávány takzvané reference na funkce (funcrefs). Zápis anonymní funkce se v několika ohledech odlišuje od zápisu běžné funkce. Parametry i samotné tělo funkce jsou totiž umístěny do složených závorek {} a mezi parametry a tělem funkce se zapisuje dvojice znaků -> (u parametrů se nemusí používat prefix a:). Anonymní funkci, která po svém zavolání vrátí součet svých dvou parametrů, lze zapsat takto:

{x,y -> x + y}

Obrázek 8: Nápověda k nové vlastnosti VimScriptu – podpora anonymních funkcí.

Volání takové funkce může vypadat například takto:

:echo {x,y -> x + y}(1,2)

Obrázek 9: Volání anonymní funkce s předáním parametrů.

Obrázek 10: Výsledek funkce je vypsán v levém dolním rohu.

Alternativně lze anonymní funkci přiřadit do proměnné (a tak ji vlastně pojmenovat):

:let Fce={x,y -> x + y}
:echo Fce(1,2)
3

Poznámka: z anonymních funkcí lze vytvořit i uzávěr (closure). Příklad si ukážeme příště.

6. Reference na funkce

V předchozí kapitole jsme se setkali s termínem „reference na funkci“ či zkráceně „funcref“. Nejedná se sice o nový koncept zavedený až ve Vimu 8, ale vzhledem k tomu, že se s referencemi na funkce setkáme i v několika dalších demonstračních příkladech, si stručně vysvětlíme, o co se vlastně jedná. Reference na funkci obsahuje odkaz na existující funkci (přičemž nás nezajímá, jak konkrétně je tento odkaz reprezentován) a lze ji považovat za plnohodnotný datový typ skriptovacího jazyka VimScript, což znamená, že reference na funkce lze ukládat do proměnných (globálních i lokálních) či je předávat do jiných funkcí (takzvaných funkcí vyššího řádu). Reference na existující funkci se získá zavoláním function(jméno_funkce), tedy například následujícím způsobem:

function! DoubleValue(index, value)
    return a:value * 2
endfunction
 
let Funcref = function("DoubleValue")

Poznámka: povšimněte si, že se reference na funkci odlišuje od jména funkce, zatímco v jiných programovacích jazycích se jedná o totožný objekt.

Ukázku použití reference na funkci si ukážeme ihned v následující kapitole.

7. Příklad číslo 3: použití reference na funkci společně s funkcí vyššího řádu map

Ještě před ukázkou použití anonymních funkcí (lambd) se seznámíme se dvěma užitečnými funkcemi, které je možné ve VimScriptu použít. Jedná se o funkce nazvané map a filter. Tyto funkce jsou nazývány funkcemi vyššího řádu, a to z toho důvodu, že jako svůj parametr akceptují jiné funkce, přesněji řečeno buď anonymní funkci či funcref. Funkce vyššího řádu map dokáže aplikovat zvolenou anonymní funkci či funcref na seznam (list) či na slovník (dictionary). Podívejme se, jak použití map vypadá v praxi. Nejdříve vytvoříme seznam obsahující sekvenci celých čísel od 0 do 9 a následně na prvky této sekvence postupně aplikujeme uživatelskou funkci DoubleValue vracející dvojnásobnou hodnotu svého parametru. Ve skutečnosti jsou do funkce DoubleValue předány dva parametry – index prvku a jeho hodnota:

" ---------------------------------------------
" Vim8 example script #3 - usage of map()
"
" How to use it:
" 1) start new Vim session
" 2) open this script in it
" 3) call :source %
" ---------------------------------------------

function! DoubleValue(index, value)
    return a:value * 2
endfunction

let  sequence = range(10)
echo sequence

let  Funcref = function("DoubleValue")

call map(sequence, Funcref)
echo sequence

call map(sequence, Funcref)
echo sequence

Obrázek 11: Výsledek předchozího příkladu.

Poznámka: povšimněte si, že funkce map ve VimScriptu modifikuje původní seznam, čímž se odlišuje od podobně koncipovaných funkcí známých z jiných programovacích jazyků:

8. Příklad číslo 4: použití anonymních funkcí s funkcí vyššího řádu map

Předchozí příklad lze snadno přepsat tak, aby se v něm namísto uživatelské funkce DoubleValue použila anonymní funkce, která vrátí dvojnásobnou hodnotu svého druhého parametru (první parametr je ignorován). Tuto funkci můžeme zapsat následujícím způsobem:

{index, value -> value * 2}

Podívejme se nyní, jak se zápis celého příkladu zkrátí díky tomu, že můžeme anonymní funkci vložit přímo do volání funkce vyššího řádu map:

" ---------------------------------------------
" Vim8 example script #4 - lambda expressions
"
" How to use it:
" 1) start new Vim session
" 2) open this script in it
" 3) call :source %
" ---------------------------------------------

let  sequence = range(10)
echo sequence

call map(sequence, {index, value -> value * 2})
echo sequence

call map(sequence, {index, value -> value * 2})
echo sequence

Obrázek 12: Výsledek spuštění předchozího příkladu.

9. Příklad číslo 5: funkce map neměnící vstupní sekvenci

Chování funkce vyššího řádu map ve VimScriptu může vývojáře zaskočit kvůli již zmíněnému faktu, že tato funkce přímo mění obsah seznamu či slovníku, který je jí předán. Náprava je ve skutečnosti poměrně jednoduchá, protože postačuje, aby se seznam/slovník před předáním do této funkce naklonoval pomocí další užitečné funkce nazvané příhodně copy. Předchozí skript tedy můžeme upravit do „funkcionální podoby“ takto:

" ---------------------------------------------
" Vim8 example script #5 - lambda expressions
"
" How to use it:
" 1) start new Vim session
" 2) open this script in it
" 3) call :source %
" ---------------------------------------------

let  sequence = range(10)
echo "sequence1=" sequence
echo "\n"

let  sequence2 = map(copy(sequence), {index, value -> value * 2})
echo "sequence1=" sequence
echo "sequence2=" sequence2
echo "\n"

let  sequence3 = map(copy(sequence2), {index, value -> value * 2})
echo "sequence1=" sequence
echo "sequence2=" sequence2
echo "sequence3=" sequence3

Obrázek 13: Původní seznamy nazvané sequence a sequence2 jsou nyní nezměněny.

10. Příklad číslo 6: použití anonymních funkcí s funkcí vyššího řádu filter

Druhá velmi užitečná funkce vyššího řádu, kterou lze použít společně s anonymními funkcemi, se jmenuje filter a slouží pro výběr těch prvků ze seznamu či ze slovníku, které odpovídají nějaké uživatelsky definované podmínce. Tato podmínka má tvar takzvaného predikátu, což je funkce vracející pravdivostní hodnotu true či false na základě hodnoty předaného prvku. V dalším demonstračním skriptu je nejprve opět vytvořen seznam s prvky 0 až 9 a následně jsou z tohoto seznamu vybrány prvky dělitelné dvěma a posléze dělitelné třemi. Pro zápis predikátu opět využijeme novou vlastnost Vimu 8 – anonymní funkce. Aby se původní seznam nezměnil, je opět nutné vytvořit jeho klon s využitím funkce copy:

" ---------------------------------------------
" Vim8 example script #6 - lambda expressions
"
" How to use it:
" 1) start new Vim session
" 2) open this script in it
" 3) call :source %
" ---------------------------------------------

let  sequence = range(10)
echo "sequence1=" sequence
echo "\n"

let  sequence2 = filter(copy(sequence), {index, value -> value % 2 == 0})
let  sequence3 = filter(copy(sequence), {index, value -> value % 3 == 0})
echo "sequence1=" sequence
echo "sequence2=" sequence2
echo "sequence3=" sequence3

Obrázek 14: Použití funkce vyššího řádu filter pro výběr prvků ze seznamu.

11. Časovače

S anonymními funkcemi a funcrefy souvisí i další nová vlastnost Vimu 8. Jedná se o takzvané časovače (timers), které umožňují specifikovat, v jakém okamžiku se zavolá nějaká běžná či anonymní funkce. Tyto funkce jsou spouštěny asynchronně, tj. nezávisle na dalším běhu skriptu. Navíc je možné jednoduše specifikovat, kolikrát se má daná funkce zavolat, popř. je možné zajistit, aby se funkce volala periodicky až do ukončení běhu Vimu (to může být velmi užitečné, například u složitějších pluginů). Nový časovač se vytvoří zavoláním funkce nazvané příhodně timer_start. Této funkci je nutné předat minimálně dva parametry: časový interval specifikovaný v milisekundách a anonymní funkci či funcref, která se má po uplynutí daného intervalu zavolat:

timer_start(interval, funcref či anonymní funkce)

Taktéž je možné do timer_start předat třetí nepovinný parametr, kterým je slovník obsahující další upřesnění funkce časovače. V současnosti je podporována jen jediná vlastnost – počet opakování (repeat) popř. specifikace periodického opakování volání funkce:

timer_start(interval, funcref či anonymní funkce, {'repeat':počet_opakování})

Periodické opakování používá magickou konstantu -1:

timer_start(interval, funcref či anonymní funkce, {'repeat':-1})

Funkce timer_start vrátí jednoznačný identifikátor právě vytvořeného časovače.

12. Příklad číslo 7: vytvoření a použití několika časovačů

O tom, že je základní použití časovačů vlastně velmi jednoduché, se lze snadno přesvědčit prozkoumáním následujícího příkladu, v němž jsou vytvořeny čtyři časovače volající uživatelskou funkci nazvanou PrintMessage. První časovač zavolá tuto funkci celkem šestkrát s periodou jedné sekundy, druhý časovač jedenkrát po 3,3 sekundách atd. Po spuštění tohoto skriptu sledujte zprávy vypisované do levého spodního rohu editoru:

" ---------------------------------------------
" Vim8 example script #7 - timers
"
" How to use it:
" 1) start new Vim session
" 2) open this script in it
" 3) call :source %
" ---------------------------------------------

function! PrintMessage(message)
    echo a:message
endfunction

call PrintMessage("normal call")

let timer1 = timer_start(1000, 'PrintMessage', {'repeat':6})
echo "timer" timer1 "created"

let timer2 = timer_start(3300, 'PrintMessage')
echo "timer" timer2 "created"

let timer3 = timer_start(4400, 'PrintMessage')
echo "timer" timer3 "created"

let timer4 = timer_start(5500, 'PrintMessage')
echo "timer" timer4 "created"

Vylepšenou ukázku použití časovačů si popíšeme příště.

13. Repositář s demonstračními příklady

Všechny v předchozích kapitolách popsané demonstrační skripty určené pro textový editor Vim verze 8 byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/presentations. V následující tabulce naleznete na jednotlivé skripty přímé odkazy:

Příklad Adresa
windows1.vim https://github.com/tisnik/presentations/blob/master/vim/vim8/windows1.vim
windows2.vim https://github.com/tisnik/presentations/blob/master/vim/vim8/windows2.vim
map.vim https://github.com/tisnik/presentations/blob/master/vim/vim8/map.vim
lambda1.vim https://github.com/tisnik/presentations/blob/master/vim/vim8/lambda1.vim
lambda2.vim https://github.com/tisnik/presentations/blob/master/vim/vim8/lambda2.vim
lambda3.vim https://github.com/tisnik/presentations/blob/master/vim/vim8/lambda3.vim
timers.vim https://github.com/tisnik/presentations/blob/master/vim/vim8/timers.vim

14. Odkazy na Internetu

  1. how to get list of open windows
    http://vim.1045645.n5.nabble.com/how-to-get-list-of-open-windows-td1164662.html
  2. Vim – the editor
    http://www.vim.org/
  3. Vim 8.0 is released
    https://laravel-news.com/2016/09/vim-8-0-is-released/
  4. Vim: So long Pathogen, hello native package loading
    https://shapeshed.com/vim-packages/
  5. :smile command was not backported! #5116
    https://github.com/neovim/neovim/issues/5116
  6. Makejob
    http://www.vim.org/scripts/script.php?script_id=5479
  7. Články o Vimu na Root.cz:
    http://www.root.cz/n/vim/clanky/
  8. Vim sedm - první část
    http://www.root.cz/clanky/vim-sedm-prvni-cast/
  9. vim (man page)
    http://www.linux-tutorial.info/modules.php?name=ManPage&sec=1&manpage=vim
  10. History of the Text Editor
    http://vanstee.me/history-of-the-text-editor.html
  11. Interview with Bill Joy
    http://web.cecs.pdx.edu/~kirkenda/joy84.html
  12. vi Editor Commands
    http://www.cs.rit.edu/~cslab/vi.html#A1.4
  13. vi Manual
    http://www.cs.fsu.edu/general/vimanual.html
  14. Mastering the Vi Editor
    http://www.susnet.uk/mastering-the-vi-editor
  15. Vim as a Python IDE, or Python IDE as Vim
    http://blog.jetbrains.com/pycharm/2013/06/vim-as-a-python-ide-or-python-ide-as-vim/
  16. Vi Improved
    https://wiki.python.org/moin/Vim
  17. Popis skriptu Vim Pathogen
    http://www.vim.org/scripts/script.php?script_id=2332
  18. Poslední­verze skriptu Vim Pathogen
    https://tpo.pe/pathogen.vim
  19. Nejlepší pluginy pro Vim
    http://vimawesome.com/
  20. Nejlepší pluginy pro Vim
    http://www.vim.org/scripts/script_search_results.php?order_by=rating
  21. Building Vim
    http://vim.wikia.com/wiki/Building_Vim
  22. Vim plugins for developers
    http://www.linuxtoday.com/upload/vim-plugins-for-developers-140619094010.html