Header Photo credit to Elijah Porter
View The Talk
I presented this material at the New York Vim Meetup. View the presentation below:
Here I Dreamt I Was An Architect
"Design is not making beauty, beauty emerges from selection, affinities, integration, love." ~ Louis Kahn
The Four Freedoms Park (image above) is an architectural masterpiece designed by Louis Khan. The park's architecture embellishes its riverfront property which lies between Manhattan and Long Island. Instead of competing with the environment, Khan often designs architecture which leverages it.
This same principles define well crafted Vimscript. Celebrated Vimscript authors (e.g. tpope, tommcdo, and AndrewRadev) compliment vim's features with their plugins. Design at this level requires a comprehensive understanding of vim, and its swath of functionality. Great Vimscript feels natural to vim users. This is just as important as writing something useful.
If limited to one piece of advice to Vimscript authors, I would offer this: Compliment your environment, do not compete with it. This article builds off of this concept, and extends advice on how to author great Vimscript.
Building A Foundation
Conceptualizing certain ideas is a prerequisite to authoring exceptional Vimscript. These next sections offer a variety of such ideas. The aim is to prime the mind into recognizing good problems to solve with Vimscript, and to compress the learning curve of the language.
Customization Over Origination
Most Vimscripts serve one of the two following functions:
- Customize/extend built-in settings
- Enhance the editor with brand new functionality
In either situation, a single principal prevails: Straying from Vim's convictions often creates more work, and a less polished solution. It's not hard to find code that breaks this rule, and it hurts the community when code like gets released. It bloats the codebase, and teaches bad habits.
Customization Example: File Browsing
NerdTree is a popular plugin that enables file browsing a' la TextMate's project drawer. The plugin duplicates a large amount of existing code (netrw), and has a significant functionality flaw. Drew Neil eloquently phrases how project drawers and Vim splits mix like Oil and vinegar.
Another option, vim-vinegar plays nicely with splits and leverages existing netrw code. Instead of reinventing functionality, vinegar improves netrw with slick configurations.
What is the gain?
Qualitatively, vim-vinegar feels more natural to a user adapted to vim's native functionality.
Quantitatively, vim-vinegar is 5x smaller than NerdTree (529 kb vs 2775 kb).
Resources
:h netrw
- vim-vinegar
- Oil and vinegar
- NerdTree
Augmentation Example: Swapping Text
Yet, there are circumstances which vim provides no feasable solution. A great example is swapping text. Examine the example swapping "bar" and "baz" using nothing but stock vim commands. It amounts to 15 keystrokes, and to revert the operation requires 2 undo commands.
"foo [b]ar baz"
(viwy w viwp bb viwp)
"foo [baz] bar"
The plugin vim-exchange, wraps this functionality into one swift command. Examine the same operation with vim-exchange (9 keystrokes, and 1 undo).
"foo [b]ar baz"
(cxiw w cxiw)
"foo [baz] bar"
A key to the design of this plugin is interface to the end user.
The mapping cx
does not clobber any vim functionality.
The command is an operator so things like cxaw
and cxx
work.
These details make the plugin flow with vim.
References
- vim-exchange
:h operator
Cause And Effect
Vim is event focused. It's waiting for things to happen (sequences of keypresses). When one such trigger occurs, actions fire. Callbacks respond to these events.
Functions bind to key sequences (mappings, commands, abbreviations) and events (autocommands). Here are some examples:
nmap ge ibeep<cr>
iabbrev beep boop
command Beep :normal!("iboop")
autocmd FileType markdown echo('beepboop')
Choosing the right event a piece of Vimscript should bind to can make the design more clean / useful.
Resources
:h map.txt
- Learn Vimscript The Hard Way - Basic Mapping
:h abbrev
- Learn Vimscript The Hard Way - Abbreviations
:h command
:h normal
- Learn Vimscript The Hard Way - Normal
- Learn Vimscript The Hard Way - Execute Normal!
:h autocmd
- Learn Vimscript The Hard Way - Autocommands
:h echo
- Learn Vimscript The Hard Way - Echoing Messages
On Learning Syntax
This article refrains from explicitly covering Vimscript syntax. Attepts at writing a tutorial would be subpar to these more comprehsive tutorials:
- The harder / quicker path:
:h usr_41.txt
- The easier / slower path: Learn Vimscript the Hard Way
Each resource provides detailed information on syntax and technical details required when writing Vimscript. They each take a unique approach to essentially the same material. After reading either piece, you should be comfortable with basic Vimscript. Albeit, less palpable aspects may still be insipid. The aim of this article is to fill some of those missing pieces.
The remaining sections offer tidbits of wisdom I aquired through experience. One section covers Vimscript itself, while the other targets writing plugins.
Resources
:h usr_41.txt
- Learn Vimscript the Hard Way
Hacking Vim Script
Once you know Vimscript, opportunities for productivity boosts emerge. This section presents some ideas that will assist your efforts to learn Vimscript.
Don't Prematurely Write Plugins
Write Vimscript for yourself before broadcasting it to the world. When starting out it's important to write a lot of actual code. Plugins require documentation, and degrees of design that bring their own challenges. This will take away from the learning process of the language.
Poking The Box
Obviously, Vimscript is best written within vim. Doing this offers conveniences that are impossible to recreate in another editor. Namely, a gateway to an instant feedback loop via runtime evaluation. Instead of guessing how a piece of code will behave, vim supports trying it immediately. Below are a handful of methods for evaluating Vimscript at runtime.
Evaluating Files
The primary runtime evaluation tool is the :source
command.
This command accepts a path to a file as an argument, and loads it into vim's runtime.
This avoids having to close and open vim again.
:source ~/.vim/init/keybindings.vim
Pro Tips
- Add
!
to the end of functions, allowing them to reload. :so
is the same as:source
:source %
will load the current file
Here are some reloadable code examples:
nmap ge :echo('beep')<CR>
iabbrev beep boop
command! Beep normal! iboop
function! BeepBoop()
echo('beep')
endfunction
Resources
:h so
:h function
- Learn Vimscript The Hard Way - Functions
Inspecting
Command mode is not a REPL for Vimscript. As such, other means of interaction need to exist. The simplest strategy is echo.
:let g:beep='boop'
:g:beep
:echo(g:beep)
:1 + 1
:echo(1+1)
It's useful to echo out variables, but can get cumbersome with longer experssions. Tpope's scriptease plugin offers some facilities to allieviate this.
The g!
is an operator that replaces the text with evaluated Vimscript.
For example, running g!!
on the line 1 + 1
would change the line to 2
.
:PP
opens a REPL for evaluating Vimscript.
:PP
PP> system('ls')
"CNAME\n_config.yml\n_includes\n_layouts\n_posts\n_publish\n_site\nassets\nbin\ncontact.html\ncss\nfeed\nfonts\nimages\nimg\nindex.html\njs\nnode_modules\npac
kage.json\nresume.html\nsass\nsrc\ntest\ntodo.txt\n"
Resources
:h command
:h g:
- Learn Vimscript The Hard Way - Variables
- Learn Vimscript The Hard Way - Variable Scoping
:h echo
- Learn Vimscript The Hard Way - Echoing Messages
- scriptease
:h scriptease
Use The OS
The operating system provides a lot of functionality, and its smart to leverage it.
Calling system
exposes unix commands to vim.
echo(system('ls'))
echo(system('ls -lal '.expand('%')))
Resources
:h system
- Learn Vimscript The Hard Way - External Commands
:h expand
State Up To Date
The key to Vimscript is understanding how to manipulate vim's state. There are 3 main ways to interact with application state in vim:
- Passively reading current state
- Updating state
- Binding to an event broadcasted when state changes
Most Vimscript functionality employs one of these interactions.
Reading State
Variables govern the state and behavior of vim. Variable namespacing is systematic in Vimscript. Here are the different types of variables:
g:variables - global variables
Globals preserve value in any situation within vim.
This means any mode, and any buffer.
A frequent use of these variables is to memoize plugin loading.
:echo(g:loaded_fugitive)
> 1
s:variables - script local
These variables have scope limited to their file.
This allows 2 scripts to have the same variable, but not collide.
let s:counter = 0
function! MyCounter()
let s:counter = s:counter + 1
echo(s:counter)
endfunction
b:variables - buffer local
These variables bind to the current buffer.
Syntax specific functionality is a great usecase for them.
:echo(b:did_ftplugin)
> 1
$VARIABLES - Environment
All environment variables carry over from the shell.
Like the shell, dollar signs ($) prefix environment variables.
:echo($PAGER)
> less
&variables - Set options
Calling set filetype=markdown
is a wrapper to vim's option variables.
:echo(&tabstop)
> 2
@variable - register variables
The registers are global containers to save values.
Macros save into single digit global registers(0-9a-z).
Access to these registers via @
in Vimscript.
:let @d='ggdG'
:echo(@d)
> ggdG
Buffer State
Some buffer state is not available via variables. This makes vim faster because it does not need to constantly write changing data. It will lazy-load this information via certain functions it provides. Consider this example of reading the current line:
:echo(line('.'))
> 262
Resources
:h variables
- Learn Vimscript The Hard Way - Variables
:h line
Updating State
Whenever you interact with vim you are updating runtime state.
A call to :set filetype=markdown
will update the &filetype
variable.
Typing gg
will move the current line.
:w
tells vim to execute its buffer writing sequence.
State updates are similar in Vim and Vimscript. The main ways to update state are as follows:
(Note .
concats strings in Vimscript)
let - updates a variables state
let &filetype='markdown'
let g:script_loaded=1
execute - evaluates a string as an ex (:) command
execute ":w ".expand('%')
normal! - executes commands as if a user typed them
normal! gg=G
execute "normal! ".commands
eval - evaluates a string as Vimscript
eval('let @r="gg=G")
Resources
:h :set
- Learn Vimscript The Hard Way - Setting Options
:h :let
:h :execute
- Learn Vimscript The Hard Way - Execute
:h :normal
- Learn Vimscript The Hard Way - Normal
- Learn Vimscript The Hard Way - Execute Normal!
:h :eval
:h :expand
Listening For State Changes
Vim is constantly changing state. Your code can listen to these events by subscribing to them.
Keyboard Events
Vim has 2 main modes for subscribing to keyboard events: mappings, and abbreviations. Mappings can be global, or mode-specific:
:map <f12> :h<cr>
:imap <c-c> <esc>:w<cr>
:nmap ZJ JZZ
Abbreviations work in insert, command, and replace mode. The tell vim to replace one set of text with another.
cabbrev W w
abbrev teh the
iabbrev yo you
Autocommands
Events fire in vim when stuff happens. Vimscript functions can bind to these events. Autocommands expose this functionality (see the Cause and Effect section above).
Resources
:h map
- Learn Vimscript The Hard Way - Basic Mapping
:h abbrev
- Learn Vimscript The Hard Way - Abbreviations
:h autocmd
- Learn Vimscript The Hard Way - Autocommands
Plugin Design / Best Practices
After achieving comfort with Vimscript, the next logical step is to write useful scripts and share them with the world. The easiest way to share is via plugins.
Structuring
In general, there are 2 main types of plugins: global (fugitive, splitjoin, vim-exchange), and filetype specific (vim-rails). The structure can vary, but here is a simplified template for each:
Global Plugin
- doc
- autoload
- plugin
For a global plugin, the general formula is simple: expose your plugin within the plugin directory, and autoload everything else.
When starting out, autoload may be overkill, but read :h autoload
to prime your brain to use it in the future.
FT Plugin
Filetype plugins generally have a bit more meat to them. Ftdetect evaluates the filetype. Syntax outlines rules for vims syntax highlighter. Ftplugin is where buffer specific code resides.
Loading
Plugins should memoize themselves (unless under development). To make sure they don't load twice, guard files with a snippet like this:
if exists("g:loaded_gitgutter")
finish
endif
let g:loaded_gitgutter = 1
The Deal With SID and Plug
SID
Plugins can leverage the <SID>
feature offered by Vim.
Imagine this circumstance:
""""""""""""""""""
"pluginA.vim
""""""""""""""""""
function! BeepBoop()
echo('beep')
endfunction
nmap ge :call BeepBoop()<cr>
""""""""""""""""""
"pluginB.vim
""""""""""""""""""
function! BeepBoop()
echo('boop')
endfunction
In this situation the mapping from pluginA, could actually call BeepBoop from pluginB depinding on the order in which scripts load.
To prevent this, the <SID>
prefix expands to the <SNR>
of the file.
""""""""""""""""""
"pluginA.vim
""""""""""""""""""
function! s:BeepBoop()
echo('beep')
endfunction
nmap ge :call <SID>BeepBoop()<cr>
""""""""""""""""""
"pluginB.vim
""""""""""""""""""
function! s:BeepBoop()
echo('boop')
endfunction
Vim would expand that snippet to be this:
"pluginA.vim
function! <SNR>48_BeepBoop()
echo('beep')
endfunction
nmap ge :call <SNR>48_BeepBoop()<cr>
"pluginB.vim
function! <SNR>_87BeepBoop()
echo('boop')
endfunction
<SID>
and function s:name
make function names more explicit, and reliable.
Plug
The vim documentation says <Plug>
is a "special code that a typed key will never produce".
Unlike <SID>, <PLUG>
is available to the global namespace.
It can expose a function call to the global environment.
Alas, a user can access that function elsewhere.
This allows them to attach their own keybinding to that function.
This globally living <Plug>
code is the same for all plugins.
As such, use conventionally named commands like <Plug>PluginnameFunc
to avoid collisions.
"""""""""""
"plugin.vim
"""""""""""
noremap <unique> <Plug>PluginFunc :call <SID>VimScriptFn()<CR>
""""""""""""""""
"keybindings.vim
""""""""""""""""
:nmap _p <Plug>ScriptFunc
Resources
:h plugins
- Learn Vimscript The Hard Way - Plugin Layout in the Dark Ages
- Learn Vimscript The Hard Way - Detecting Filetypes
- Learn Vimscript The Hard Way - Basic Syntax Highlighting
Conclusion
Vimscript is a useful language that one can learn in a short time.
The aim of this article is to highlight the best ways to use it.
When used in conjunction with :h usr_41.txt
or Learn Vimscript the Hard Way it should guide the reader to write exceptionally crafted Vimscript.
To reiterate the mantra from the beginning of this post: Compliment your environment, do not compete with it.
And, most of all have fun!!!
Please feel free to post questions in the comments section below.
Resources
- Learn Vimscript the Hard Way
- NerdTree
- Oil and vinegar
- scriptease
- vim-vinegar
:h :eval
:h :execute
:h :expand
:h :let
:h :normal
:h :set
:h abbrev
:h autocmd
:h command
:h echo
:h expand
:h function
:h g:
:h line
:h map.txt
:h map
:h netrw
:h normal
:h scriptease
:h so
:h system
:h usr_41.txt
:h variables