How to create a VIM Plugin

Thomas Brisbout

Freelance JS developer

 tbrisbout

Vim Paris Meetup - 2015-12-14

Contents

  • Introduction
  • Vimscript basics
  • Structure
  • Patterns
  • Plugin Examples
  • Publication

Introduction

Why ?

  • Does this need a plugin ?
  • Is there an existing one ?
  • Can I do It ?
  • What kind of Plugin ?
  • What language ?
  • How to Publish it ?

Can I do it ?

When I first wrote vim-bufferline and vim-airline I was very much a newbie Vim scripter

@bling

Types of Plugins

  • Motions / Shortcuts / text-objects
  • CLI wrapper
  • Language Support Plugin
  • New Functionality (Ctrl-p / NERDTree...)

Little bit of Vimscript

Properties

  • Interpreted
  • Main paradigm: procedural 
    • functional paradigm possible with some helpers (haya14busa/underscore.vim, :help deepcopy())
    • basic prototype-OOP with dictionary type
  • Dynamic Typing

Basics

" Variable assignment
let x = ''
let x = 2

" Conditionals
if x
  echo 'ok'
elseif y 
  echo 'not ok'
else
  echo 'ko'
endif

" Loops
for i in [1, 2, 3, 4]
  echo i
endfor

Strings

" Normal Strings
echo "This is a string"
echo "whith some\n escape chars\n"

" Literal Strings
echo 'A literal \ String'

" Concatenation
echo "Hello " . "World"

Lists

echo ['a', 3, 'c']

let l = [1, 2, 3]
echo l[1] "=> 2
echo l[-1] "=> 3

" Concat
let concat = [1, 2] + [3]


" Slicing
echo ['a', 'b', 'c', 'd', 'e'][0:2]
" => ['a', 'b', 'c']

Lists Functions

" Modifiers mutates the list
get()			get an item without error for wrong index
len()			number of items in a List
empty()			check if List is empty
insert()		insert an item somewhere in a List
add()			append an item to a List
extend()		append a List to a List
remove()		remove one or more items from a List
copy()			make a shallow copy of a List
deepcopy()		make a full copy of a List
filter()		remove selected items from a List
map()			change each List item
sort()			sort a List
reverse()		reverse the order of a List
split()			split a String into a List
join()			join List items into a String
range()			return a List with a sequence of numbers
string()		String representation of a List
call()			call a function with List as arguments
index()			index of a value in a List
max()			maximum value in a List
min()			minimum value in a List
count()			count number of times a value appears in a List
repeat()		repeat a List multiple times

Dictionaries

" Keys are string 
" or coerced to string

echo {'foo': 1, 100: 'bar'}
" => {'foo': 1, '100': 'bar'}

Dictionaries Functions

get()			get an entry without an error for a wrong key
len()			number of entries in a Dictionary
has_key()		check whether a key appears in a Dictionary
empty()			check if Dictionary is empty
remove()		remove an entry from a Dictionary
extend()		add entries from one Dictionary to another
filter()		remove selected entries from a Dictionary
map()			change each Dictionary entry
keys()			get List of Dictionary keys
values()		get List of Dictionary values
items()			get List of Dictionary key-value pairs
copy()			make a shallow copy of a Dictionary
deepcopy()		make a full copy of a Dictionary
string()		String representation of a Dictionary
max()			maximum value in a Dictionary
min()			minimum value in a Dictionary
count()			count number of times a value appears

Functions

" Function declaration
function HelloWorld()
  echo "Hello World!"
endfunction

" use ! to override
function! HelloWorld()
  ...
endfunction

" With arguments
function! HelloMe(name)
  echo "Hello " . a:name
endfunction

" Invocation
call HelloWorld()
echo HelloWorld()

Scoping

(nothing) In a function: local to a function; 
          otherwise: global
b: Local to the current buffer.
w: Local to the current window.
t: Local to the current tab page.
g: Global.
l: Local to a function.
s: Local to a |:source|'ed Vim script.
a: Function argument (only inside a function).
v: Global, predefined by Vim.

Vimscript WTF?!

" String coercion
echo "foo" + "bar"
" => 0
echo "10foo" + 5 
" => 15

echo "10foo" + "5bar"

" in conditionals
if "foo"
  echo "OK"
elseif "123foo"
  echo "Wait, what !?"
else 
  echo "why not..."
endif

Vimscript WTF again...

" Case sensitive comparisons
echo "foo" == "FOO"
" => 0

set ignorecase
echo "foo" == "FOO"
" => 1

" Specific operator
set ignorecase
echo "foo" ==# "FOO"
" => 0

Structure

Simple Plugin Skeleton

" 1 - Mappings
nnoremap K :MyPlugin

" 2 - Functions
function! MyPlugin()
  " do something
endfunction

" Commands
command! MyPlugin call MyPlugin()

Project Structure

project/
|
├── after/
├── autoload/
├── colors/
├── compiler/
├── doc/
├── ftdetect/
├── ftplugin/
├── indent/
├── plugin/
├── LICENSE.md
└── README.md

ftdetect

  • Run once when vim starts
  • Used to set a filetype autocommand

ftplugin

  • Run each time a filetype is set
    • So each time a file is opened with a given filetype (ex: myfiletype) the ftplugin/filetype.vim runs
  • Useful to define file specific mappings and functions

plugin

  • Run once when vim starts
  • Most of the logic can go here if the plugin is simple

autoload

  • Hack used to perform lazy-loading
  • The file is run when the function is first called and then cached
" Autoloaded function syntax
" This will look for ./autoload/myplugin.vim
call myplugin#MyFunc()

Patterns and tips

Useful mappings

" Time-saving mapping
nnoremap <leader>s :source %

" Even better
nnoremap <leader>s :w | source %

" Even even better
augroup vim_scripts
  autocmd!
  autocmd Filetype vim nnoremap <buffer> <leader>s :w | source %
augroup END

" Useful Plugin
Plugin 'tpope/vim-scriptease'

Execute Normal!

" Common pattern
execute "normal! gg/foo<cr>"

Global Setting

" User configuration
if !exists("g:plugin_option")
  let g:plugin_option = "Default Value"
endif

Good Practices

  • Do not overwrite registers / settings / etc...
  • Use function namespaces

Plugin Examples

Better Grep

nnoremap <leader>g :set operatorfunc=<SID>GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call <SID>GrepOperator(visualmode())<cr>

function! s:GrepOperator(type)
  let saved_unnamed_register = @@

  if a:type ==# 'v'
    execute "normal! `<v`>y"
  elseif a:type ==# 'char'
    execute "normal! `[v`]y"
  else 
    return
  endif

  silent execute "grep! -R " . shellescape(@@) . " ."
  copen

  let @@ = saved_unnamed_register
endfunction

Babel CLI wrapper 1/2

" ./ftdetect/es6.vim

autocmd BufNewFile,BufRead *.es6 set filetype=javascript

Babel CLI wrapper 2/2

" ./plugin/babeljs.vim

if !exists("g:babeljs_command")
  let g:babeljs_command = "babel"
endif

if !exists("g:babeljs_presets")
  let g:babeljs_presets = "es2015"
endif

function! BabelES5()
  let file = expand('%')
  execute "new | r !" . g:babeljs_command . " --presets " . g:babeljs_presets. " " . file
  set filetype=javascript
  normal! ggdd
endfunction

command! Babel call BabelES5()

Publication

Source

  • Github
  • vim.org

Choose a License

  • VIM License
  • MIT / Apache 2
  • Other (WTFYWT...)

Documentation

  • doc in .txt and README.md
  • What the plugin does
  • Setup (Pathogen compatible)
  • API
  • Mapping example
  • FAQ
  • Thanks ...

Some links

  • http://learnvimscriptthehardway.stevelosh.com/
  • http://vimdoc.sourceforge.net/htmldoc/usr_41.html
  • http://www.ibm.com/developerworks/linux/library/l-vim-script-1/index.html
  • Every plugin from Tim Pope ...

How to create a VIM Plugin

By Thomas BRISBOUT

How to create a VIM Plugin

This is an introduction to Neovim (Project Status, installation and configuration tips ...)

  • 1,474