Making a Vim plugin with Python
Why using Python in a Vim plugin
Vimscript is a well-designed language that is really tailored to extend Vim. Unfortunately, this language is not very powerful and quite clunky for general-purpose computing.
Fortunately, it is possible to use other languages in your Vim plugin. For example, Python.
Python is very easy to prototype in, and fast to write and its extensive standard library is a great help when writing Vim plugins. Thus, it is a good choice for our plugins.
Making a Vim plugin
Before making a plugin using Python, let's remind ourselves how to do a simple plugin. Our plugin will be contained in a directory named with the name of our plugin. For example `test_plugin`.
This directory will be in the plugin directory of our plugin manager. For example, with Pathogen[1], `test_plugin` will be in `<vim dir>/bundle`. For the rest of this post, the plugin manager's directory will be written `<plugin manager dir>`.
In our plugin's directory, we will create a directory named `plugin` where the Vimscript code is stored. Scripts stored there will be run when the plugin manager loads plugins. There can be other directories such as `autoload` but their use is outside of the scope of this post.
Let's write a small plugin that prints the content of the variable `g:myVar` when pressing `<F1>`. The variable will be initialized to "abcd".
`<plugin manager dir>/test_plugin/plugin/test_plugin.vim`:
" Variable initialization
let g:myVar = "abcd"
function PrintMyVar()
" Expand to `normal a` which is the command to write the content of myVar in the buffer
let l:exec = "normal a" . g:myVar
" Run the command we defined
execute l:exec
endfunction
" Binds to the function we defined
nnoremap :call PrintMyVar()
Now, when running Vim, pressing `<F1>` will write "abcd" as this is the behavior of the plugin.
Calling a python script in the vim plugin
Let's now make a very simple script that prints "Hello, world!" when starting Vim. But this will have a quirk, the printing will be made in a Python script. The Python script will be in the `plugin` directory of our plugin.
`<plugin manager dir>/test_plugin/plugin/hello_world.py`:
print("Hello, world!")
We will write a Vim script that executes the Python script when starting Vim. The first challenge is to find the path of the python script. This can be done with the Vimscript function `expand('<sfile>:p:h')` which finds the directory of the script executing this function. Then, we will use `py3file` to run the script.
`<plugin manager dir>/test_plugin/plugin/hello_world.vim`:
" Finding the path of the current script
let s:script_dir = expand(':p:h')
" Generating the py3file command that runs the script
let s:python_exec_cmd = 'py3file ' . s:script_dir . "/hello_world.py"
" Executing it
execute s:python_exec_cmd
We are now greeted with a slightly annoying "Hello, world!" when starting Vim. But the annoyance is sweetened by the knowledge that this is executed with Python code.
Vim module
In our Python script, to interact with Vim, we must use the module `vim`. This module can do a lot of things that we can read with Vim's included documentation with `:help python-vim`.
Communication between Vimscript and Python
To make a more complex plugin, we will need to exchange data between Vimscript code and Python code. To do so, we need to use Vim's global variable that can be accessed from Python with `vim.vars`.
Let's improve the script `test_plugin.vim` so that it writes twice the content of `g:myVar`. We will start by making a Python script that duplicates the content of `g:myVar` and write it into `g:myBigVar`.
`<plugin manager dir>/test_plugin/plugin/hello_world.py`:
import vim
def duplicate_string(s):
"Return the double of the input string."
return f"{s}{s}"
vim.vars["myBigVar"] = duplicate_string(vim.vars["myVar"].decode()) # Note that the content of vim.vars is byte arrays so we need to use `decode`
Then, we will rewrite `test_plugin.vim` to use the new Python script.
`<plugin manager dir>/test_plugin/plugin/test_plugin.vim`:
" Variables initialization
let g:myVar = "abcd"
let s:script_dir = expand(':p:h')
function PrintMyVar()
" Generating the py3file command that runs the script
let s:python_exec_cmd = 'py3file ' . s:script_dir . "/script.py"
" Executing it
execute s:python_exec_cmd
" Expand to `normal a` which is the command to write the content of myBigVar in the buffer
let l:exec = "normal a" . g:myBigVar
" Run the command we defined
execute l:exec
endfunction
" Binds to the function we defined
nnoremap :call PrintMyVar()
Using multiple Python files
For an even more complex script, we will want to have multiple Python files that we use with `import`. To do so, we need to add the path of our scripts to Python's library path list. This can be done by putting the path of the scripts in one of Vim's global variables.
`<plugin manager dir>/test_plugin/plugin/test_plugin.vim`:
" Variables initialization
let g:myVar = "abcd"
let g:test_plugin_dir = expand(':p:h')
function PrintMyVar()
" Generating the py3file command that runs the script
let s:python_exec_cmd = 'py3file ' . g:test_plugin_dir . "/script.py"
" Executing it
execute s:python_exec_cmd
" Expand to `normal a` which is the command to write the content of myBigVar in the buffer
let l:exec = "normal a" . g:myBigVar
" Run the command we defined
execute l:exec
endfunction
" Binds to the function we defined
nnoremap :call PrintMyVar()
We will then put the `duplicate_string` function in a separate file.
`<plugin manager dir>/test_plugin/plugin/my_lib.py`:
def duplicate_string(s):
"Return the double of the input string."
return f"{s}{s}"
Lastly, we will change `script.py` so that it uses the lib.
`<plugin manager dir>/test_plugin/plugin/script.py`:
import sys import vim sys.path.append(vim.vars["test_plugin_dir"].decode()) import my_lib vim.vars["myBigVar"] = my_lib.duplicate_string(vim.vars["myVar"].decode()) # Note that the content of vim.vars is byte arrays so we need to use `decode`
Conclusion
We are now ready to write complex Vim plugins with a mix of Vimscript and Python.