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
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)
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
12. Příklad číslo 7: vytvoření a použití několika časovačů
13. Repositář s demonstračními příklady
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:
14. Odkazy na Internetu
- how to get list of open windows
http://vim.1045645.n5.nabble.com/how-to-get-list-of-open-windows-td1164662.html - Vim – the editor
http://www.vim.org/ - Vim 8.0 is released
https://laravel-news.com/2016/09/vim-8-0-is-released/ - Vim: So long Pathogen, hello native package loading
https://shapeshed.com/vim-packages/ - :smile command was not backported! #5116
https://github.com/neovim/neovim/issues/5116 - Makejob
http://www.vim.org/scripts/script.php?script_id=5479 - Články o Vimu na Root.cz:
http://www.root.cz/n/vim/clanky/ - Vim sedm - první část
http://www.root.cz/clanky/vim-sedm-prvni-cast/ - vim (man page)
http://www.linux-tutorial.info/modules.php?name=ManPage&sec=1&manpage=vim - History of the Text Editor
http://vanstee.me/history-of-the-text-editor.html - Interview with Bill Joy
http://web.cecs.pdx.edu/~kirkenda/joy84.html - vi Editor Commands
http://www.cs.rit.edu/~cslab/vi.html#A1.4 - vi Manual
http://www.cs.fsu.edu/general/vimanual.html - Mastering the Vi Editor
http://www.susnet.uk/mastering-the-vi-editor - 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/ - Vi Improved
https://wiki.python.org/moin/Vim - Popis skriptu Vim Pathogen
http://www.vim.org/scripts/script.php?script_id=2332 - Posledníverze skriptu Vim Pathogen
https://tpo.pe/pathogen.vim - Nejlepší pluginy pro Vim
http://vimawesome.com/ - Nejlepší pluginy pro Vim
http://www.vim.org/scripts/script_search_results.php?order_by=rating - Building Vim
http://vim.wikia.com/wiki/Building_Vim - Vim plugins for developers
http://www.linuxtoday.com/upload/vim-plugins-for-developers-140619094010.html