Snake (SNAAAAAAAAAKKKE) is a Python module for Vim that let's you use Python to its fullest extent to enhance Vim.
Here's an example of a helper function, written in Python, using Snake, to
toggle the word under your cursor between snake-case and camel-case when you
press <leader>c
:
import snake
@snake.key_map("<leader>c")
def toggle_snake_case_camel_case():
""" take the word under the cursor and toggle it between snake-case and
camel-case """
word = snake.get_word()
# is it snake case?
if "_" in word:
chunks = word.split("_")
camel_case = chunks[0] + "".join([chunk.capitalize() for chunk in
chunks[1:]])
snake.replace_word(camel_case)
# is it camel case?
else:
# split our word on capital letters followed by non-capital letters
chunks = filter(None, re.split("([A-Z][^A-Z]*)", word))
snake_case = "_".join([chunk.lower() for chunk in chunks])
snake.replace_word(snake_case)
When you press the key pattern in key_map
, the decorated function will run.
import snake
@snake.key_map("<leader>r")
def reverse():
snake.replace_word(snake.get_word()[::-1])
A Python function can be expanded dynamically as you type an abbreviation in insert mode.
import snake
import time
snake.abbrev("curtime", time.ctime)
Sometimes it is convenient to run some code when the buffer you open is of a specific file type.
import snake
@snake.when_buffer_is("python")
def setup_python_folding(ctx):
ctx.set_option("foldmethod", "indent")
ctx.set_option("foldnestmax", 2)
ctx.key_map("<space>", "za")
A context object is passed into the function you wrap. This context allows you to set options, let variables, and create abbreviations and keymaps that apply only to the buffer you just opened, not globally.
from snake import keys
def uppercase_second_word():
keys("gg") # go to top of file, first character
keys("w") # next word
keys("viw") # select inner word
keys("~") # uppercase it
Your Vim version must include +python
to use Snake. You can check with :version
.
Add the following line to your Vundle plugin block of your .vimrc
:
Plugin 'amoffat/snake'
And the following lines to the end of the file:
if filereadable(expand("~/.vim/bundle/snake/plugin/snake.vim"))
source ~/.vim/bundle/snake/plugin/snake.vim
endif
Re-source your .vimrc
. Then :PluginInstall
TODO
Add the following line to your Neobundle plugin block of your .vimrc
:
NeoBundle 'amoffat/snake'
And the following lines to the end of the file:
if filereadable(expand("~/.vim/bundle/snake/plugin/snake.vim"))
source ~/.vim/bundle/snake/plugin/snake.vim
endif
Re-source your .vimrc
. Then NeoBundleInstall
~/.vimrc.py
is intended to be the python equivalent of ~/.vimrc
. Snake will
load and evaluate it on startup. It should contain all of your Snake
initialization code and do any imports of other Snake plugins. If were so
inclined, you could move all of your vim settings and options into ~/.vimrc.py
as well:
from snake import multi_set_option, let, multi_command
multi_set_option(
"nocompatible",
"exrc",
"secure",
("background", "dark"),
("textwidth", 80),
("shiftwidth", tab),
("softtabstop", tab),
("tabstop", tab),
"expandtab",
)
let("mapleader", ",")
multi_command(
"nohlsearch",
"syntax on",
)
from snake.plugins import my_rad_plugin
Vim is powerful, but its commands and key-bindings are built for seemingly every
use case imaginable. It doesn't distinguish between commonly-used and
rarely-used. Snake should use that existing foundation of functionality to add
structure for commonly-needed operations. For example, many vim users know that
yiw
yanks the word you're on into a register. This is a common operation, and
so it should be mapped to a simple function:
@preserve_state()
def get_word():
keys("yiw")
return get_register("0")
Now instead of your plugin containing execute "normal! yiw"
, it can contain
word = get_word()
If your plugin is a package, create (or symlink) a directory inside
~/.vim/bundle
for your plugin. Make this directory a Python package by
creating a __init__.py
If your plugin is a simple one-file module, just create or symlink that file
into your ~/.vim/bundle
directory.
Next Add from snake.plugins import <your_plugin>
to ~/.vimrc.py
. Finally,
Re-source your ~/.vimrc
For plugin API reference, check out api_reference.md.
Yes! But it's crazy!
Just include a requirements.txt
file in your package directory that contains
the pip freeze
output of all the dependencies you wish to include. When your
module is imported from .vimrc.py
, a virtualenv will be automatically created
for your plugin if it does not exist, and your plugin dependencies automatically
installed.
Virtualenvs that are created automatically will use your virtualenv_wrapper
WORKON_HOME
environment variable, if one exists, otherwise ~/.virtualenvs
.
And virtualenvs take the name snake_plugin_<your_plugin_name>
.
You may be wondering how snake can allow for different virtualenvs for different plugins within a single Python process. There's a little magic going on, and as such, there are some gotchas.
When a plugin with a virtualenv is imported, it is imported automatically within that plugin's virtualenv. Then the virtualenv is exited. This process is repeated for each plugin with a virtualenv.
What this means is that all of your plugin's imports must occur at your plugin's import time:
GOOD:
from snake import *
import requests
def stuff():
return requests.get("http://google.com").text
BAD:
from snake import *
def stuff():
import requests
return requests.get("http://google.com").text
The difference here is that in the first example, your plugin will have a
reference to the correct requests
module, because it was imported while your
plugin was being imported inside its virtualenv. In the second example, when
stuff()
runs, it is no longer inside of your plugin's virtualenv, so when it
imports requests
, it will not get the correct module or any module at all.
There is also the problem of different plugins having different dependency
versions. For example, if Snake plugin A
depends on sh==1.10
and plugin B
depends on sh==1.11
, whichever plugin gets loaded first in .vimrc.py
will
put their sh
module into sys.modules
. Then, when the other plugin loads,
it will attempt to load sh
, see it is in sys.modules
, and use that instead,
instead of looking in its virtualenv.
All of this obviously isn't great, and something better needs to be built to
more thoroughly separate virtualenvs from under a single Python process. I
think what can happen is, for the SnakePluginHook
, if a fullname
has more
than 3 paths, drop into the virtualenv for the plugin and run imp.find_module
.
If the module exists, return self
as the loader. Repeat the process in
load_module
except actually imp.load_module
. This way, the dependency
should be loaded into sys.modules
prefixed by the full plugin name
snake.plugins.whatever.sh
.
Read development.md for technical info.
Although Snake is meant to make Vim more scriptable in Python, it is not meant to provide all the nuanced functionality of Vim. PRs for new features will be screened by the value-add of the feature weighed against the complexity added to the api, with favor towards keeping the api simple.
I would like to see an import hook that allows this in your .vimrc.py
:
from snake import *
something_awesome = __import__("snake.plugins.tpope/something_awesome")
Where the import hook checks if the plugin exists in ~/.vim/snake
, and if it
doesn't, looks for a repo to clone at
https://github.com/tpope/something_awesome