├── .travis.yml ├── CHANGELOG.md ├── README.md ├── docs ├── api_reference.md └── development.md ├── logo.gif ├── plugin ├── bootstrap.py ├── prelude.vim ├── snake.vim └── snake │ ├── __init__.py │ └── plugin_loader.py └── tests.py /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | 4 | language: python 5 | 6 | python: 7 | - 2.7 8 | 9 | before_script: 10 | - sudo add-apt-repository ppa:fcwu-tw/ppa -y 11 | - sudo apt-get update -qq 12 | - sudo apt-get install -qq vim 13 | - pip install sh 14 | 15 | script: 16 | - python tests.py 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.15.4 - 3/3/18 4 | * bugfix with old pip version creating virtualenvs 5 | * bugfix with python3 not having execfile when installing virtualenvs 6 | 7 | ## 0.15.3 - 3/1/18 8 | * add `set_filetype` for easily associating extensions to file types 9 | 10 | ## 0.15.2 - 2/14/18 11 | * `delete_word` shouldn't preserve cursor, and it should return word 12 | 13 | ## 0.15.1 - 2/4/18 14 | * bugfix in escaping strings containing single quotes 15 | * support for `search` only searching current line 16 | * bugfix where snake and plugins were not being reloaded on resourcing vimrc 17 | 18 | ## 0.15.0 - 2/3/18 19 | * python3 support 20 | 21 | ## 0.14.3 - 5/16/17 22 | * make visual selection callback argument optional 23 | * bugfix where mode preserving context can yield annoying errors 24 | 25 | ## 0.14.2 - 5/16/17 26 | * bugfix in restore order of state preservation contexts 27 | 28 | ## 0.14.1 - 5/13/17 29 | * `preserve_mode` actually does something now 30 | * added `debug(msg, persistent=False)` helper for scripts 31 | * bugfix where a fn decorated with `key_map` wasn't propagating additional options. 32 | 33 | ## 0.14.0 - 4/8/17 34 | * Fixed `vim.buffers` indexing inconsistency between vim versions 35 | * Added polyfill for missing pyeval in older vim versions 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![snake logo](https://github.com/amoffat/snake/blob/master/logo.gif) 2 | 3 | [![Build 4 | Status](https://travis-ci.org/amoffat/snake.svg?branch=master)](https://travis-ci.org/amoffat/snake) 5 | 6 | Snake (SNAAAAAAAAAKKKE) is a Python module for Vim that let's you use Python to 7 | its fullest extent to enhance Vim. 8 | 9 | Here's an example of a helper function, written in Python, using Snake, to 10 | toggle the word under your cursor between snake-case and camel-case when you 11 | press `c`: 12 | 13 | ```python 14 | import snake 15 | 16 | @snake.key_map("c") 17 | def toggle_snake_case_camel_case(): 18 | """ take the word under the cursor and toggle it between snake-case and 19 | camel-case """ 20 | 21 | word = snake.get_word() 22 | 23 | # is it snake case? 24 | if "_" in word: 25 | chunks = word.split("_") 26 | camel_case = chunks[0] + "".join([chunk.capitalize() for chunk in 27 | chunks[1:]]) 28 | snake.replace_word(camel_case) 29 | 30 | # is it camel case? 31 | else: 32 | # split our word on capital letters followed by non-capital letters 33 | chunks = filter(None, re.split("([A-Z][^A-Z]*)", word)) 34 | snake_case = "_".join([chunk.lower() for chunk in chunks]) 35 | snake.replace_word(snake_case) 36 | ``` 37 | 38 | ![Metal Gear Solid Snake Success](http://i.imgur.com/ZFr3vXG.gif) 39 | 40 | ## [Full API Reference](docs/api_reference.md) 41 | 42 | # Some other cool things you can do 43 | 44 | ## Bind a function to a key 45 | 46 | When you press the key pattern in ``key_map``, the decorated function will run. 47 | 48 | ```python 49 | import snake 50 | 51 | @snake.key_map("r") 52 | def reverse(): 53 | snake.replace_word(snake.get_word()[::-1]) 54 | ``` 55 | 56 | ## Use a function for an abbreviation 57 | 58 | A Python function can be expanded dynamically as you type an abbreviation in 59 | insert mode. 60 | 61 | ```python 62 | import snake 63 | import time 64 | 65 | snake.abbrev("curtime", time.ctime) 66 | ``` 67 | 68 | ## Have a function run for a file type 69 | 70 | Sometimes it is convenient to run some code when the buffer you open is of a 71 | specific file type. 72 | 73 | ```python 74 | import snake 75 | 76 | @snake.when_buffer_is("python") 77 | def setup_python_folding(ctx): 78 | ctx.set_option("foldmethod", "indent") 79 | ctx.set_option("foldnestmax", 2) 80 | ctx.key_map("", "za") 81 | ``` 82 | 83 | A context object is passed into the function you wrap. This context allows you 84 | to set options, let variables, and create abbreviations and keymaps that apply 85 | only to the buffer you just opened, not globally. 86 | 87 | ## Press arbitrary keys as if you typed them 88 | 89 | ```python 90 | from snake import keys 91 | 92 | def uppercase_second_word(): 93 | keys("gg") # go to top of file, first character 94 | keys("w") # next word 95 | keys("viw") # select inner word 96 | keys("~") # uppercase it 97 | ``` 98 | 99 | # Installation 100 | 101 | Your Vim version must include [`+python`](http://vimdoc.sourceforge.net/htmldoc/various.html#+python) to use Snake. You can check with `:version`. 102 | 103 | ## Vundle 104 | 105 | Add the following line to your Vundle plugin block of your `.vimrc`: 106 | 107 | ``` 108 | Plugin 'amoffat/snake' 109 | ``` 110 | 111 | And the following lines to the end of the file: 112 | ``` 113 | if filereadable(expand("~/.vim/bundle/snake/plugin/snake.vim")) 114 | source ~/.vim/bundle/snake/plugin/snake.vim 115 | endif 116 | ``` 117 | 118 | Re-source your `.vimrc`. Then `:PluginInstall` 119 | 120 | ## Pathogen 121 | 122 | TODO 123 | 124 | ## Neobundle 125 | 126 | Add the following line to your Neobundle plugin block of your `.vimrc`: 127 | 128 | ``` 129 | NeoBundle 'amoffat/snake' 130 | ``` 131 | 132 | And the following lines to the end of the file: 133 | ``` 134 | if filereadable(expand("~/.vim/bundle/snake/plugin/snake.vim")) 135 | source ~/.vim/bundle/snake/plugin/snake.vim 136 | endif 137 | ``` 138 | 139 | Re-source your `.vimrc`. Then `NeoBundleInstall` 140 | 141 | # Where do I write my Snake code? 142 | 143 | `~/.vimrc.py` is intended to be the python equivalent of `~/.vimrc`. Snake will 144 | load and evaluate it on startup. It should contain all of your Snake 145 | initialization code and do any imports of other Snake plugins. If were so 146 | inclined, you could move all of your vim settings and options into `~/.vimrc.py` 147 | as well: 148 | 149 | ```python 150 | from snake import multi_set_option, let, multi_command 151 | 152 | multi_set_option( 153 | "nocompatible", 154 | "exrc", 155 | "secure", 156 | 157 | ("background", "dark"), 158 | ("textwidth", 80), 159 | 160 | ("shiftwidth", tab), 161 | ("softtabstop", tab), 162 | ("tabstop", tab), 163 | "expandtab", 164 | ) 165 | 166 | let("mapleader", ",") 167 | 168 | multi_command( 169 | "nohlsearch", 170 | "syntax on", 171 | ) 172 | 173 | from snake.plugins import my_rad_plugin 174 | ``` 175 | 176 | # Design Philosophy 177 | 178 | Vim is powerful, but its commands and key-bindings are built for seemingly every 179 | use case imaginable. It doesn't distinguish between commonly-used and 180 | rarely-used. Snake should use that existing foundation of functionality to add 181 | structure for commonly-needed operations. For example, many vim users know that 182 | `yiw` yanks the word you're on into a register. This is a common operation, and 183 | so it should be mapped to a simple function: 184 | 185 | ```python 186 | @preserve_state() 187 | def get_word(): 188 | keys("yiw") 189 | return get_register("0") 190 | ``` 191 | 192 | Now instead of your plugin containing `execute "normal! yiw"`, it can contain 193 | `word = get_word()` 194 | 195 | 196 | # How do I write a plugin? 197 | 198 | If your plugin is a package, create (or symlink) a directory inside 199 | `~/.vim/bundle` for your plugin. Make this directory a Python package by 200 | creating a `__init__.py` 201 | 202 | If your plugin is a simple one-file module, just create or symlink that file 203 | into your `~/.vim/bundle` directory. 204 | 205 | Next Add `from snake.plugins import ` to `~/.vimrc.py`. Finally, 206 | Re-source your `~/.vimrc` 207 | 208 | For plugin API reference, check out [api\_reference.md](docs/api_reference.md). 209 | 210 | # Can I use a virtualenv for my plugin? 211 | 212 | Yes! But it's crazy! 213 | 214 | Just include a `requirements.txt` file in your package directory that contains 215 | the `pip freeze` output of all the dependencies you wish to include. When your 216 | module is imported from `.vimrc.py`, a virtualenv will be automatically created 217 | for your plugin if it does not exist, and your plugin dependencies automatically 218 | installed. 219 | 220 | Virtualenvs that are created automatically will use your virtualenv\_wrapper 221 | `WORKON_HOME` environment variable, if one exists, otherwise `~/.virtualenvs`. 222 | And virtualenvs take the name `snake_plugin_`. 223 | 224 | ## Gotchas 225 | 226 | You may be wondering how snake can allow for different virtualenvs for different 227 | plugins within a single Python process. There's a little magic going on, and as 228 | such, there are some gotchas. 229 | 230 | When a plugin with a virtualenv is imported, it is imported automatically within 231 | that plugin's virtualenv. Then the virtualenv is exited. This process is 232 | repeated for each plugin with a virtualenv. 233 | 234 | What this means is that all of your plugin's imports *must* occur at your 235 | plugin's import time: 236 | 237 | GOOD: 238 | ```python 239 | from snake import * 240 | import requests 241 | 242 | def stuff(): 243 | return requests.get("http://google.com").text 244 | ``` 245 | 246 | BAD: 247 | ```python 248 | from snake import * 249 | 250 | def stuff(): 251 | import requests 252 | return requests.get("http://google.com").text 253 | ``` 254 | 255 | The difference here is that in the first example, your plugin will have a 256 | reference to the correct `requests` module, because it was imported while your 257 | plugin was being imported inside its virtualenv. In the second example, when 258 | `stuff()` runs, it is no longer inside of your plugin's virtualenv, so when it 259 | imports `requests`, it will not get the correct module or any module at all. 260 | 261 | There is also the problem of different plugins having different dependency 262 | versions. For example, if Snake plugin `A` depends on `sh==1.10` and plugin `B` 263 | depends on `sh==1.11`, whichever plugin gets loaded first in `.vimrc.py` will 264 | put *their* `sh` module into `sys.modules`. Then, when the other plugin loads, 265 | it will attempt to load `sh`, see it is in `sys.modules`, and use that instead, 266 | instead of looking in its virtualenv. 267 | 268 | All of this obviously isn't great, and something better needs to be built to 269 | more thoroughly separate virtualenvs from under a single Python process. I 270 | think what can happen is, for the `SnakePluginHook`, if a `fullname` has more 271 | than 3 paths, drop into the virtualenv for the plugin and run `imp.find_module`. 272 | If the module exists, return `self` as the loader. Repeat the process in 273 | `load_module` except actually `imp.load_module`. This way, the dependency 274 | should be loaded into `sys.modules` prefixed by the full plugin name 275 | `snake.plugins.whatever.sh`. 276 | 277 | 278 | # Contributing 279 | 280 | Read [development.md](docs/development.md) for technical info. 281 | 282 | ## Pull requests 283 | 284 | Although Snake is meant to make Vim more scriptable in Python, it is *not* meant 285 | to provide all the nuanced functionality of Vim. PRs for new features will be 286 | screened by the value-add of the feature weighed against the complexity added to 287 | the api, with favor towards keeping the api simple. 288 | 289 | 290 | ## Snake needs a vundle equivalent 291 | 292 | I would like to see an import hook that allows this in your `.vimrc.py`: 293 | 294 | ```python 295 | from snake import * 296 | 297 | something_awesome = __import__("snake.plugins.tpope/something_awesome") 298 | ``` 299 | 300 | Where the import hook checks if the plugin exists in `~/.vim/snake`, and if it 301 | doesn't, looks for a repo to clone at 302 | `https://github.com/tpope/something_awesome` 303 | -------------------------------------------------------------------------------- /docs/api_reference.md: -------------------------------------------------------------------------------- 1 | TODO please help document this stuff 2 | 3 | # Core 4 | 5 | ### keys(k) 6 | 7 | `k` is a string of valid key presses to send to Vim. These key presses can 8 | include navigation, yanking, deleting, etc. This is a core function to much of 9 | the functionality Snake provides. 10 | 11 | ### command(cmd, capture=False) 12 | 13 | Runs `cmd` as a Vim command, as if you typed `:cmd`. If `capture` is `True`, 14 | also return the output of the command. 15 | 16 | ### expand(stuff) 17 | 18 | Expands Vim wildcards and keywords. For example, `expand("%:p")` will return 19 | the current file as an absolute path. See also: 20 | http://vimdoc.sourceforge.net/htmldoc/eval.html#expand() 21 | 22 | # Cursor 23 | 24 | ### get_cursor_position() 25 | 26 | Returns the current cursor position as a tuple, `(row, column)`. 27 | 28 | ### set_cursor_position(pos) 29 | 30 | Sets the cursor position, where `pos` is a tuple `(row, column)`. 31 | 32 | # State Management 33 | 34 | These context managers and decorators help keep your functions from messing 35 | around with your current state. For example, if you had a function that 36 | searched for and counted the number of times "bananas" was said in your buffer, 37 | you would want to use the context manager `with preserve_cursor():` in order to 38 | keep your cursor in the same location before and after your function runs. 39 | 40 | ### preserve_state() 41 | 42 | A convenience decorator that preserves common states. For example: 43 | 44 | ```python 45 | @preserve_state 46 | def do_something(): 47 | pass 48 | ``` 49 | 50 | Calling `do_something()` would be as if you called it like this: 51 | 52 | ```python 53 | with preserve_cursor(), preserve_mode(), preserve_registers(): 54 | do_something() 55 | ``` 56 | 57 | ### preserve_cursor() 58 | 59 | A with-context manager that preserves the cursor location 60 | 61 | ### preserve_buffer() 62 | 63 | A with-context manager that preserves the current buffer contents. 64 | 65 | ### preserve_mode() 66 | 67 | A with-context manager that doesn't do anything because apparently it's 68 | ridiculously difficult/impossible to get the current mode (visual mode, normal 69 | mode, etc). Feel free to grind mind against this one if you want to get it 70 | working. 71 | 72 | ### preserve_registers(\*regs) 73 | 74 | A with-context manager that preserves the registers listed in `\*regs`, along 75 | with the special delete register and default yank register. Use it like this: 76 | 77 | ```python 78 | with preserve_registers("a"): 79 | keys('"ayy') 80 | yanked_line = get_register("a") 81 | ``` 82 | 83 | # Convenience 84 | 85 | * get_word() 86 | * delete_word() 87 | * replace_word(rep) 88 | * get_in_quotes() 89 | * get_leader() 90 | * get_runtime_path() 91 | * set_runtime_path(paths) 92 | * multi_command(\*cmds) 93 | * set_filetype(pat, ftype) 94 | 95 | # Visual 96 | 97 | ### get_visual_selection() 98 | 99 | Returns the content currently selected in visual mode. 100 | 101 | ### replace_visual_selection(rep) 102 | 103 | Replaces the content currently selected in visual mode with `rep`. 104 | 105 | ### get_visual_range() 106 | 107 | Gets the `((start_row, start_col), (end_row, end_col))` of the visual selection. 108 | 109 | # Input 110 | 111 | * raw_input(prompt="") 112 | 113 | # Buffers 114 | 115 | * new_buffer(name, type=BUFFER_SCRATCH) 116 | * get_buffers() 117 | * set_buffer(buf) 118 | * get_current_buffer() 119 | * get_buffer_in_window(win) 120 | * get_num_buffers() 121 | * set_buffer_contents(buf, s) 122 | * set_buffer_lines(buf, lines) 123 | * get_buffer_contents(buf) 124 | * get_current_buffer_contents() 125 | * get_buffer_lines(buf) 126 | * when_buffer_is(filetype) 127 | 128 | # Windows 129 | 130 | * get_current_window() 131 | * get_num_windows() 132 | * get_window_of_buffer(buf) 133 | * new_window(size=None, vertical=False) 134 | 135 | 136 | # Variables 137 | 138 | ### let(name, value, namespace=None, scope=NS_GLOBAL) 139 | 140 | Sets a variable. You typically only need the `name` and `value`. You can use 141 | different scopes for the variable, like `NS_GLOBAL` ("g") or buffer-local scope. 142 | `namespace` follows the convention of many Vim plugins by prefixing a Vim 143 | variable name with the plugin name. So something like: 144 | 145 | ```python 146 | let("switch_buffer", "0", namespace="ctrlp") 147 | ``` 148 | 149 | Seems not super useful now, but it comes in handy with `multi_let`, where you 150 | can define many plugin variables at once. 151 | 152 | ### get(name, namespace=None, scope=NS_GLOBAL) 153 | 154 | Gets a variable's value. 155 | 156 | ### multi_let(namespace, **name_values) 157 | 158 | Let's you batch-define a bunch of variables related to some namespace. It's 159 | essentially a sequence of `let`s, where the namespace of all of them is the 160 | same. For example, in my `.vimrc.py`: 161 | 162 | ```python 163 | multi_let( 164 | "ctrlp", 165 | match_window="bottom,order:tbb", 166 | switch_buffer=0, 167 | user_command='ag %s -l --nocolor -U --hidden -g ""', 168 | working_path_mode="r", 169 | map="", 170 | cmd="CtrlPBuffer", 171 | ) 172 | ``` 173 | 174 | This sets all of my `ctrlp` settings in one go. 175 | 176 | # Options 177 | 178 | ### set_option(name, value=None) 179 | 180 | Sets a Vim option, like: 181 | 182 | ```python 183 | set_option("expandtab") 184 | set_option("textwidth", 80) 185 | ``` 186 | 187 | ### get_option(name) 188 | 189 | Gets an option's value. 190 | 191 | ### toggle_option(name) 192 | 193 | Toggles an option on and off. 194 | 195 | ### multi_set_option(\*names) 196 | 197 | A convenience function for batch setting options in your `.vimrc.py`: 198 | 199 | ```python 200 | multi_set_option( 201 | "nocompatible", 202 | "exrc", 203 | "secure", 204 | 205 | ("background", "dark"), 206 | ("textwidth", 80), 207 | 208 | ("shiftwidth", tab), 209 | ("softtabstop", tab), 210 | ("tabstop", tab), 211 | "expandtab", 212 | 213 | "incsearch", 214 | "hlsearch", 215 | ) 216 | ``` 217 | 218 | ### set_option_default(name) 219 | ### unset_option(name) 220 | ### set_local_option(name, value=None) 221 | 222 | Sets a buffer-local option. 223 | 224 | # Registers 225 | 226 | * get_register(name) 227 | * set_regsiter(name, value) 228 | * clear_register(name) 229 | 230 | 231 | # Key Mapping 232 | 233 | ### key_map(keys, fn_or_command, mode=NORMAL_MODE, recursive=False) 234 | 235 | Maps the key-sequence `keys` to a Vim key-sequence or a Python function. To use 236 | it like you would in regular Vim: 237 | 238 | ```python 239 | key_map("sv", ":source $MYVIMRC") 240 | ``` 241 | 242 | Or to use it with a Python function: 243 | 244 | ```python 245 | key_map("t", sync_to_network) 246 | ``` 247 | 248 | `key_map` can also optionally be used as a decorator: 249 | 250 | ```python 251 | @key_map("t") 252 | def sync_to_network(): 253 | """ sync this buffer to a remote machine """ 254 | ``` 255 | 256 | ### visual_key_map(key, fn, recursive=False) 257 | 258 | Creates a key mapping for visual mode. What's cool about this function is that 259 | if you attach a Python function to the key-sequence, that function will be 260 | passed the contents of the current visual selection. And if your function 261 | returns anything other than `None`, it will be used to replace the contents of 262 | the visual selection: 263 | 264 | ```python 265 | @visual_key_map("b") 266 | def reverse_everything(selected): 267 | s = list(selected) 268 | s.reverse() 269 | return "".join(s) 270 | ``` 271 | 272 | # Searching 273 | 274 | ### search(s, wrap=True, backwards=False, move=True, curline=False) 275 | 276 | Returns the `(row, col)` of the string `s`. By default, it will move the cursor 277 | there. 278 | 279 | # Misc 280 | 281 | * redraw() 282 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | Automated Testing 2 | ================= 3 | 4 | Please create tests for any new features or bugs fixed. Take a look at 5 | `tests.py` to see how the existing tests work. Basically Vim is started in a 6 | headless mode and you can feed an and input file and a script. The script can 7 | communicate to the test. The final changed file is also available to the test. 8 | 9 | Reloading snake 10 | =============== 11 | 12 | If you've installed snake with vundle, the first thing you might notice is that 13 | re-sourcing your `.vimrc` does not reload snake, so changes you've made will not 14 | be visible in your current Vim session. To get around this, add the following 15 | line to your `.vimrc`: 16 | 17 | ``` 18 | source ~/.vim/bundle/snake/plugin/snake.vim 19 | ``` 20 | 21 | Now when you re-source `.vimrc`, snake will be reloaded, and your `.vimrc.py` 22 | will be re-evaluated. 23 | 24 | How functions work 25 | ================== 26 | 27 | One cool thing about Snake is that it allows you to attach arbitrary Python 28 | functions to Vim key mappings or abbreviations. 29 | 30 | ```python 31 | @key_map("a") 32 | def print_hello(): 33 | print("hello") 34 | ``` 35 | 36 | This is super convenient, but in order to accomplish this, we employ some 37 | trickery. Vim needs a reference to the Python function somehow, in order to 38 | call it. We use the `id()` of the function object itself to use as this 39 | reference. We then store this reference in a mapping that maps the reference id 40 | to the function object. You can see this taking place in the `register_fn(fn)` 41 | function: 42 | 43 | ```python 44 | def register_fn(fn): 45 | """ takes a function and returns a string handle that we can use to call the 46 | function via the "python" command in vimscript """ 47 | fn_key = id(fn) 48 | _mapped_functions[fn_key] = fn 49 | return "snake.dispatch_mapped_function(%s)" % fn_key 50 | ``` 51 | 52 | The return value of `register_fn(fn)` is a string of what Vim should call in 53 | order to run the registered function. 54 | 55 | The `dispatch_mapped_function` simply takes that id reference, looks up the 56 | function object, executes the function, and returns the result. 57 | 58 | The full command that Vim runs for a key mapping might look something like this: 59 | 60 | ```nnoremap a :python snake.dispatch_mapped_function(12345)``` 61 | 62 | 63 | Punching buttons 64 | ================ 65 | 66 | The first and foremost thing you probably want to do is take some of the Vim key 67 | bindings that you know already and put them into functions. This is 68 | accomplished with `keys(k)`. The string of keys to press are passed into Vim as 69 | if you pressed them yourself. Often, this is the most basic "unit" of snake 70 | functions, in that all the real work happens through specific key bindings 71 | specified in the function: 72 | 73 | ```python 74 | @preserve_state() 75 | def delete_word(): 76 | keys("diw") 77 | ``` 78 | 79 | 80 | Preserving state 81 | ================ 82 | 83 | Common state 84 | ------------ 85 | 86 | It's important that when your snake function runs, it doesn't steamroll over the 87 | user's current state, unless you're doing it intentionally. Use the 88 | `preserve_state` context manager to prevent this. By default, it preserves 89 | cursor position, and yank (0) and delete (") special registers: 90 | 91 | ```python 92 | @preserve_state() 93 | def do_something(): 94 | keys("BByw") 95 | return get_register("0") 96 | ``` 97 | 98 | The function above will appear to do nothing to the user's cursor or registers, 99 | when in reality, it jumped back 2 words and yanked a word. 100 | 101 | Register state 102 | -------------- 103 | 104 | The `preserve_state` context manager is nice for general functions, but 105 | sometimes your function will do something very complex with registers, and you 106 | wish to preserve those individual registers. In those instances, use the 107 | `preserve_registers(*regs)` with-context around your critical section: 108 | 109 | ```python 110 | def replace_visual_selection(rep): 111 | with preserve_registers("a"): 112 | set_register("a", rep) 113 | keys("gvd") 114 | keys('"aP') 115 | ``` 116 | 117 | All of the registers passed into `preserve_registers(*regs)` will be restored 118 | after the context completes. 119 | 120 | Cursor state 121 | ------------ 122 | 123 | Similarly, the `preserve_cursor` with-context preserves the position of the 124 | cursor for the duration of the block it wraps. 125 | 126 | 127 | When all else fails 128 | =================== 129 | 130 | When you can't achieve your goals with existing snake functions, you should 131 | resort to using the [vim](http://vimdoc.sourceforge.net/htmldoc/if_pyth.html) 132 | module that vim's embedded python provides for you. You can use it to eval 133 | functions/variables/registers: 134 | 135 | ```python 136 | def get_current_window(): 137 | return int(vim.eval("winnr()")) 138 | ``` 139 | 140 | Or to execute commands: 141 | 142 | ```python 143 | def set_register(name, val): 144 | val = escape_string_dq(val) 145 | vim.command('let @%s = "%s"' % (name, val)) 146 | ``` 147 | 148 | If you find yourself using `vim.eval` and `vim.command` directly, ask yourself 149 | if what you're writing can be abstracted further to a more reusable function. 150 | 151 | Escaping 152 | ======== 153 | 154 | Two helper functions are provided to escape strings you wish to pass into vim. 155 | These are `escape_string_dq` and `escape_string_sq` for escaping strings to be 156 | surrounded by double quotes and single quotes, respectively. 157 | 158 | -------------------------------------------------------------------------------- /logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoffat/snake/30edf7dd8888e6ac4d17f12a9c341baa1d839ea4/logo.gif -------------------------------------------------------------------------------- /plugin/bootstrap.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import vim 3 | sys.path.insert(0, vim.eval("s:current_path")) 4 | 5 | def purge(mod_name): 6 | for check in list(sys.modules.keys()): 7 | if check.startswith(mod_name + ".") or check == mod_name: 8 | del sys.modules[check] 9 | 10 | purge("snake") 11 | import snake 12 | -------------------------------------------------------------------------------- /plugin/prelude.vim: -------------------------------------------------------------------------------- 1 | " if there is no pyeval, we use this polyfill taken from 2 | " https://github.com/google/vim-maktaba/issues/70#issue-35289378 3 | if exists("*pyeval") 4 | else 5 | if has("python") 6 | function! Pyeval(expr) 7 | python import json 8 | python vim.command('return '+json.dumps(eval(vim.eval('a:expr')))) 9 | endfunction 10 | else 11 | function! Pyeval(expr) 12 | python3 import json 13 | python3 vim.command('return '+json.dumps(eval(vim.eval('a:expr')))) 14 | endfunction 15 | endif 16 | endif 17 | -------------------------------------------------------------------------------- /plugin/snake.vim: -------------------------------------------------------------------------------- 1 | let s:current_path=expand(":p:h") 2 | 3 | function! LoadSnake() 4 | " contains custom vimscript stuff sourced by snake and the tests 5 | exec "source " . s:current_path . "/prelude.vim" 6 | 7 | let bootstrap=s:current_path . "/bootstrap.py" 8 | 9 | if has("python") 10 | exec "pyfile " . bootstrap 11 | elseif has("python3") 12 | exec "py3file " . bootstrap 13 | else 14 | echo "No Python available!" 15 | endif 16 | endfunction 17 | 18 | call LoadSnake() 19 | 20 | " Load snake once again so that it invalidates the function mappings 21 | " from the session file. 22 | autocmd SessionLoadPost * call LoadSnake() 23 | -------------------------------------------------------------------------------- /plugin/snake/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function, unicode_literals 2 | 3 | import vim 4 | from contextlib import contextmanager 5 | from functools import wraps, partial 6 | import os 7 | import sys 8 | from os.path import expanduser, exists, abspath, join, dirname 9 | import time 10 | import inspect 11 | import re 12 | 13 | __version__ = "0.15.5" 14 | 15 | 16 | NORMAL_MODE = "n" 17 | VISUAL_MODE = "v" 18 | INSERT_MODE = "i" 19 | COMMAND_MODE = "c" 20 | 21 | BUFFER_SCRATCH = 0 22 | 23 | NS_GLOBAL = "g" 24 | NS_BUFFER = "b" 25 | 26 | PYTHON_CMD = "python" 27 | IS_PY3 = sys.version_info[0] == 3 28 | PYTHON_CMD = "python3" if IS_PY3 else "python" 29 | 30 | _LEADER_REGEX = re.compile(r"\\", re.I) 31 | _BUFFER_LIST_REGEX = re.compile(r"^\s*(\d+)\s+(.+?)\s+\"(.+?)\"", re.M) 32 | 33 | _mapped_functions = { 34 | } 35 | 36 | # if pyeval doesn't exist, we use our own, defined in prelude.vim. pyeval 37 | # doesn't exist in vim 7.3 38 | PYEVAL = "pyeval" 39 | if not bool(int(vim.eval("exists('*pyeval')"))): 40 | PYEVAL = "Pyeval" 41 | 42 | VERSION = int(int(vim.eval("v:version"))) 43 | 44 | 45 | def _get_buffer(i): 46 | """ a shim for vim buffer index inconsistencies """ 47 | # for some reason, version 7.3 indexes their vim.buffers at 0 for buffer 1. 48 | # version 704 has buffer 1 at index 1, even though len(vim.buffers) == 1. 49 | # its weird. 50 | if VERSION < 704: 51 | i -= 1 52 | return vim.buffers[i] 53 | 54 | def command(cmd, capture=False): 55 | """ wraps vim.capture to execute a vim command. if capture is true, we'll 56 | return the output of that command """ 57 | if capture: 58 | with preserve_registers("a"): 59 | vim.command("redir @a") 60 | vim.command(cmd) 61 | vim.command("redir END") 62 | out = get_register("a") 63 | else: 64 | out = None 65 | vim.command(cmd) 66 | return out 67 | 68 | 69 | def dispatch_mapped_function(key): 70 | """ this function will be called by any function mapped to a key in visual 71 | mode. because we can't tell vim "hey, call this arbitrary, possibly 72 | anonymous, callable on key press", we have a single dispatch function to do 73 | that work for vim """ 74 | try: 75 | fn = _mapped_functions[key] 76 | except KeyError: 77 | raise Exception("""unable to find mapped function with id() == %s. 78 | Something bad related to reloading has happened. Typically, this is 79 | because you set up a key_map inside of a @when_buffer_is and 80 | reloaded your ~/.vim.py. The result is that the function decorated 81 | by @when_buffer_is isn't re-run with updated key_mappings, so the 82 | key_mappings have references to old callbacks.""" % key) 83 | else: 84 | return fn() 85 | 86 | def _generate_autocommand_name(fn): 87 | """ takes a function and returns a name that is unique to the function and 88 | where it was defined. the name must be reproducible between startup calls 89 | because its for an auto command group, and we must clear the old group out 90 | when reloading 91 | 92 | http://learnvimscriptthehardway.stevelosh.com/chapters/14.html#clearing-groups 93 | """ 94 | src = None 95 | try: 96 | src = inspect.getsourcefile(fn) 97 | except TypeError: 98 | pass 99 | 100 | if not src: 101 | src = "." 102 | return src + ":" + fn.__name__ 103 | 104 | def register_fn(fn): 105 | """ takes a function and returns a string handle that we can use to call the 106 | function via the "python" command in vimscript """ 107 | fn_key = id(fn) 108 | _mapped_functions[fn_key] = fn 109 | return "snake.dispatch_mapped_function(%s)" % fn_key 110 | 111 | @contextmanager 112 | def preserve_cursor(): 113 | """ persists cursor state across context. does not work in visual mode, 114 | because visual mode has 2 cursor locations, for start and end cursors """ 115 | p = get_cursor_position() 116 | try: 117 | yield 118 | finally: 119 | set_cursor_position(p) 120 | 121 | @contextmanager 122 | def preserve_buffer(): 123 | old_buffer = get_current_buffer() 124 | try: 125 | yield 126 | finally: 127 | set_buffer(old_buffer) 128 | 129 | @contextmanager 130 | def preserve_mode(): 131 | """ prevents a change of vim mode state """ 132 | old_mode = get_mode() 133 | visual_modes = ("v", "V", "^V") 134 | 135 | try: 136 | yield 137 | finally: 138 | cur_mode = get_mode() 139 | if cur_mode == "n": 140 | if old_mode in visual_modes: 141 | keys("gv") 142 | elif cur_mode in visual_modes: 143 | if old_mode == "n": 144 | keys("\") 145 | 146 | @contextmanager 147 | def preserve_registers(*regs): 148 | """ prevents a change of register state """ 149 | old_regs = {} 150 | 151 | special_regs = ('0', '"') 152 | regs = regs 153 | for reg in regs: 154 | contents = get_register(reg) 155 | old_regs[reg] = contents 156 | clear_register(reg) 157 | 158 | # we can't do a clear on the special registers, because setting one will 159 | # wipe out the other 160 | for reg in special_regs: 161 | contents = get_register(reg) 162 | old_regs[reg] = contents 163 | 164 | 165 | try: 166 | yield 167 | finally: 168 | for reg in regs + special_regs: 169 | old_contents = old_regs[reg] 170 | if old_contents is None: 171 | clear_register(reg) 172 | else: 173 | set_register(reg, old_contents) 174 | 175 | def debug(msg, persistent=False): 176 | """ prints a msg to your lower vim command area, for debugging. if you set 177 | persistent=True, you can view your previous message by executing :messages """ 178 | msg = str(msg) 179 | cmd = "echo" 180 | if persistent: 181 | cmd = "echom" 182 | command("%s '%s'" % (cmd, escape_string_sq(msg))) 183 | 184 | 185 | def abbrev(word, expansion, local=False): 186 | """ creates an abbreviation in insert mode. expansion can be a string to 187 | expand to or a function that returns a value to serve as the expansion """ 188 | 189 | cmd = "iabbrev" 190 | if local: 191 | cmd = cmd + " " 192 | 193 | if callable(expansion): 194 | fn_str = register_fn(expansion) 195 | expansion = "=%s('%s')" % (PYEVAL, escape_string_sq(fn_str)) 196 | 197 | command("%s %s %s" % (cmd, word, expansion)) 198 | 199 | def expand(stuff): 200 | return vim.eval("expand('%s')" % escape_string_sq(stuff)) 201 | 202 | def get_current_dir(): 203 | return dirname(get_current_file()) 204 | 205 | def get_current_file(): 206 | return expand("%:p") 207 | 208 | def get_alternate_file(): 209 | return expand("#:p") 210 | 211 | def get_cur_line(): 212 | return int(vim.eval("line('.')")) 213 | 214 | def get_mode(): 215 | return vim.eval("mode(1)") 216 | 217 | def get_num_lines(): 218 | return int(vim.eval("line('$')")) 219 | 220 | def is_last_line(): 221 | row, _ = get_cursor_position() 222 | return row == get_num_lines() 223 | 224 | 225 | def get_cursor_position(): 226 | #return vim.current.window.cursor 227 | _, start_row, start_col, _ = vim.eval("getpos('.')") 228 | return int(start_row), int(start_col) 229 | 230 | def set_cursor_position(pos): 231 | """ set our cursor position. pos is a tuple of (row, col) """ 232 | full_pos = "[0, %d, %d, 0]" % (pos[0], pos[1]) 233 | command("call setpos('.', %s)" % full_pos) 234 | 235 | 236 | def preserve_state(): 237 | """ a general decorator for preserving most state, including cursor, mode, 238 | and basic special registers " and 0 """ 239 | def decorator(fn): 240 | @wraps(fn) 241 | def wrapper(*args, **kwargs): 242 | # python 2.6 doesn't support this syntax: 243 | #with preserve_cursor(), preserve_mode(), preserve_registers(): 244 | with preserve_mode(): 245 | with preserve_cursor(): 246 | with preserve_registers(): 247 | return fn(*args, **kwargs) 248 | return wrapper 249 | return decorator 250 | 251 | def escape_string_dq(s): 252 | s = s.replace('"', r'\"') 253 | return s 254 | 255 | def escape_spaces(s): 256 | s = s.replace(" ", r"\ ") 257 | return s 258 | 259 | def escape_string_sq(s): 260 | s = s.replace("'", "''") 261 | return s 262 | 263 | def reselect_last_visual_selection(): 264 | keys("gv") 265 | 266 | def multi_let(namespace, **name_values): 267 | """ convenience function for setting multiple globals at once in your 268 | .vimrc.py, all related to a plugin. the first argument is a namespace to be 269 | appended to the front of each name/value pair. """ 270 | for name, value in name_values.items(): 271 | let(name, value, namespace=namespace) 272 | 273 | def _serialize_obj(obj): 274 | if isinstance(obj, str): 275 | obj = "'%s'" % escape_string_sq(obj) 276 | # TODO allow other kinds of serializations? 277 | return obj 278 | 279 | def _compose_let_name(name, namespace, scope): 280 | if namespace: 281 | name = namespace + "_" + name 282 | return "%s:%s" % (scope, name) 283 | 284 | 285 | def let(name, value, namespace=None, scope=NS_GLOBAL): 286 | """ sets a variable """ 287 | value = _serialize_obj(value) 288 | name = _compose_let_name(name, namespace, scope) 289 | return command("let %s=%s" % (name, value)) 290 | 291 | let_buffer_local = partial(let, scope=NS_BUFFER) 292 | 293 | def get(name, namespace=None, scope=NS_GLOBAL): 294 | """ gets a variable """ 295 | try: 296 | val = vim.eval(_compose_let_name(name, namespace, scope)) 297 | except vim.error as e: 298 | val = None 299 | return val 300 | 301 | get_buffer_local = partial(get, scope=NS_BUFFER) 302 | 303 | def search(s, wrap=True, backwards=False, move=True, curline=False): 304 | """ searches for string s, returning the (row, column) of the next match, or 305 | None if not found. 'move' moves the cursor to the match, 'backwards' 306 | specifies direction, 'wrap' for if searching should wrap around the end of 307 | the file """ 308 | flags = [] 309 | if wrap: 310 | flags.append("w") 311 | else: 312 | flags.append("W") 313 | 314 | if backwards: 315 | flags.append("b") 316 | 317 | s = escape_string_sq(s) 318 | 319 | def fn(): 320 | stopline = "" 321 | if curline: 322 | line = get_cur_line() 323 | stopline = ", {}".format(line) 324 | 325 | cmd = "search('{str}', '{flags}'{stopline})".format(str=s, 326 | flags="".join(flags), stopline=stopline) 327 | line = int(vim.eval(cmd)) 328 | match = line != 0 329 | 330 | if match: 331 | return get_cursor_position() 332 | else: 333 | return None 334 | 335 | if move: 336 | pos = fn() 337 | else: 338 | with preserve_cursor(): 339 | pos = fn() 340 | 341 | return pos 342 | 343 | def get_leader(): 344 | return vim.eval("mapleader") 345 | 346 | def keys(k, keymaps=True): 347 | """ feeds keys into vim as if you pressed them """ 348 | 349 | k = escape_string_dq(k) 350 | cmd = "normal" 351 | if keymaps: 352 | # vim does not expand "\" in execute normal 353 | # here we check explicitly for for the word leader in keys, before 354 | # attempting to run get_leader(), the reason is because on some versions 355 | # of vim, vim.eval will print an "invalid expression" error if a 356 | # variable is undefined, and that will look terrible for scripts that 357 | # press keys if a user's leader is undefined 358 | if "leader" in k.lower(): 359 | k = _LEADER_REGEX.sub(get_leader() or "", k) 360 | else: 361 | cmd += "!" 362 | command('execute "%s %s"' % (cmd, k)) 363 | 364 | def get_register(name): 365 | val = vim.eval("@%s" % name) 366 | if val == "": 367 | val = None 368 | return val 369 | 370 | def clear_register(name): 371 | set_register(name, "") 372 | 373 | def set_register(name, val): 374 | val = escape_string_dq(str(val)) 375 | command('let @%s = "%s"' % (name, val)) 376 | 377 | @preserve_state() 378 | def get_word(): 379 | """ gets the word under the cursor """ 380 | keys('"0yiw') 381 | return get_register("0") 382 | 383 | def delete_word(): 384 | """ deletes the word under the cursor """ 385 | # we don't do a @preserve_state because we want the cursor to move as the 386 | # word is deleted 387 | with preserve_mode(): 388 | with preserve_registers("0"): 389 | keys('"0diw') 390 | return get_register("0") 391 | 392 | @preserve_state() 393 | def replace_word(rep): 394 | """ replaces the word under the cursor with rep """ 395 | set_register("0", rep) 396 | keys('viw"0p') 397 | 398 | @preserve_state() 399 | def get_in_quotes(): 400 | """ gets the string beneath the cursor that lies in either double or single 401 | quotes """ 402 | keys("yi\"") 403 | val = get_register("0") 404 | if val is None: 405 | keys("yi'") 406 | val = get_register("0") 407 | return val 408 | 409 | 410 | def key_map(key, maybe_fn=None, mode=NORMAL_MODE, recursive=False, 411 | local=False, **addl_options): 412 | """ a function to bind a key to some action, be it a vim action or a python 413 | function. key_map takes a vim keymapping as the first argument """ 414 | 415 | # we're using key_map as a decorator 416 | if maybe_fn is None: 417 | def wrapper(fn): 418 | key_map(key, fn, mode=mode, recursive=recursive, local=local, 419 | **addl_options) 420 | return fn 421 | return wrapper 422 | 423 | map_command = "map" 424 | if not recursive: 425 | map_command = "nore" + map_command 426 | if mode: 427 | map_command = mode + map_command 428 | 429 | if local: 430 | map_command = map_command + " " 431 | 432 | if callable(maybe_fn): 433 | fn = maybe_fn 434 | fn_takes_selection = len(inspect.getargspec(fn).args) 435 | 436 | # if we're mapping in visual mode, we're going to assume that the 437 | # function takes the contents of the visual selection. if the function 438 | # returns something, let's replace the visual selection with it. i 439 | # think these are reasonable assumptions 440 | if mode == VISUAL_MODE: 441 | old_fn = fn 442 | @wraps(fn) 443 | def wrapped(): 444 | # only if we're expecting a selection should we pass the 445 | # selection. this has side effects 446 | if fn_takes_selection: 447 | sel = get_visual_selection() 448 | rep = old_fn(sel) 449 | else: 450 | rep = old_fn() 451 | 452 | if rep is not None: 453 | replace_visual_selection(rep) 454 | if addl_options.get("preserve_selection", False): 455 | reselect_last_visual_selection() 456 | fn = wrapped 457 | 458 | call = register_fn(fn) 459 | command("%s %s :%s %s" % (map_command, key, PYTHON_CMD, call)) 460 | 461 | else: 462 | command("%s %s %s" % (map_command, key, maybe_fn)) 463 | 464 | 465 | visual_key_map = partial(key_map, mode=VISUAL_MODE) 466 | 467 | def redraw(): 468 | command("redraw!") 469 | 470 | def step(): 471 | """ simple debugging tool to see what the hell has happened in your script 472 | so far """ 473 | redraw() 474 | time.sleep(1) 475 | 476 | @preserve_state() 477 | def get_visual_range(): 478 | """ returns the start (row, col) and end (row, col) of our range in visual 479 | mode """ 480 | keys("\gv") 481 | _, start_row, start_col, _ = vim.eval("getpos('v')") 482 | start_row = int(start_row) 483 | start_col = int(start_col) 484 | with preserve_cursor(): 485 | keys("`>") 486 | end_row, end_col = get_cursor_position() 487 | return (start_row, start_col), (end_row, end_col) 488 | 489 | def set_buffer(buf): 490 | command("buffer %d" % buf) 491 | 492 | def get_current_buffer(): 493 | return int(vim.eval("bufnr('%')")) 494 | 495 | def get_num_buffers(): 496 | i = int(vim.eval("bufnr('$')")) 497 | j = 0 498 | while i >= 1: 499 | listed = bool(int(vim.eval("buflisted(%d)" % i))) 500 | if listed: 501 | j += 1 502 | i -= 1 503 | return j 504 | 505 | 506 | def get_current_window(): 507 | return int(vim.eval("winnr()")) 508 | 509 | def get_num_windows(): 510 | return int(vim.eval("winnr('$')")) 511 | 512 | def get_window_of_buffer(buf): 513 | return int(vim.eval("bufwinnr(%d)" % buf)) 514 | 515 | def get_buffer_in_window(win): 516 | return int(vim.eval("winbufnr(%d)" % win)) 517 | 518 | def new_window(size=None, vertical=False): 519 | if vertical: 520 | cmd = "vsplit" 521 | else: 522 | cmd = "split" 523 | 524 | if size is not None: 525 | cmd = str(size) + cmd 526 | 527 | command(cmd) 528 | return get_current_window() 529 | 530 | 531 | def toggle_option(name): 532 | command("set %s!" % name) 533 | 534 | def multi_set_option(*names): 535 | """ convenience function for setting a ton of options at once, for example, 536 | in your .vimrc.py file. regular strings are treated as options with no 537 | values, while list/tuple elements are considered name/value pairs""" 538 | for name in names: 539 | val = None 540 | if isinstance(name, (list, tuple)): 541 | name, val = name 542 | set_option(name, val) 543 | 544 | def set_runtime_path(parts): 545 | rtp = ",".join(parts) 546 | set_option("rtp", rtp) 547 | 548 | def get_runtime_path(): 549 | rtp = get_option("rtp") 550 | return rtp.split(",") 551 | 552 | def get_option(name): 553 | value = vim.eval("&%s" % name) 554 | return value 555 | 556 | def set_option(name, value=None, local=False): 557 | cmd = "set" 558 | if local: 559 | cmd = "setlocal" 560 | 561 | if value is not None: 562 | command("%s %s=%s" % (cmd, name, value)) 563 | else: 564 | command("%s %s" % (cmd, name)) 565 | 566 | def set_option_default(name): 567 | command("set %s&" % name) 568 | 569 | def unset_option(name): 570 | command("set no%s" % name) 571 | 572 | def set_local_option(name, value=None): 573 | if value is not None: 574 | command("setlocal %s=%s" % (name, value)) 575 | else: 576 | command("setlocal %s" % name) 577 | 578 | def _parse_buffer_flags(flags): 579 | mapping = { 580 | "u": "unlisted", 581 | "%": "current", 582 | "#": "alternate", 583 | "a": "active", 584 | "h": "hidden", 585 | "=": "readonly", 586 | "+": "modified", 587 | "x": "errors", 588 | } 589 | parsed = {} 590 | for k,name in mapping.items(): 591 | parsed[name] = k in flags 592 | return parsed 593 | 594 | def get_buffers(): 595 | """ gets all the buffers and the data associated with them """ 596 | out = command("ls", capture=True) 597 | match = _BUFFER_LIST_REGEX.findall(out) 598 | buffers = {} 599 | if match: 600 | for (num, flags, name) in match: 601 | num = int(num) 602 | buffers[num] = { 603 | "name": name, 604 | "flags": _parse_buffer_flags(flags) 605 | } 606 | return buffers 607 | 608 | def new_buffer(name, type=BUFFER_SCRATCH): 609 | """ creates a new buffer """ 610 | # creating a new buffer will switch to it, so we need to preserve our 611 | # current buffer 612 | with preserve_buffer(): 613 | command("new") 614 | name = escape_string_sq(name) 615 | name = escape_spaces(name) 616 | command("file %s" % name) 617 | 618 | if type is BUFFER_SCRATCH: 619 | set_local_option("buftype", "nofile") 620 | set_local_option("bufhidden", "hide") 621 | set_local_option("noswapfile") 622 | 623 | buf = get_current_buffer() 624 | return buf 625 | 626 | @preserve_state() 627 | def get_visual_selection(): 628 | keys("\gvy") 629 | val = get_register("0") 630 | return val 631 | 632 | def replace_visual_selection(rep): 633 | with preserve_registers("a"): 634 | set_register("a", rep) 635 | keys("gvd") 636 | if is_last_line() and False: 637 | keys('"ap') 638 | else: 639 | keys('"aP') 640 | 641 | def set_buffer_contents(buf, s): 642 | set_buffer_lines(buf, s.split("\n")) 643 | 644 | def set_buffer_lines(buf, l): 645 | b = _get_buffer(buf) 646 | b[:] = l 647 | 648 | def get_current_buffer_contents(): 649 | return get_buffer_contents(get_current_buffer()) 650 | 651 | def get_buffer_contents(buf): 652 | contents = "\n".join(get_buffer_lines(buf)) 653 | return contents 654 | 655 | def get_buffer_lines(buf): 656 | b = _get_buffer(buf) 657 | return list(b) 658 | 659 | def raw_input(prompt=""): 660 | """ designed to shadow python's raw_input function, because it behaves the 661 | same way, except in vim """ 662 | command("call inputsave()") 663 | stuff = vim.eval("input('%s')" % escape_string_sq(prompt)) 664 | command("call inputrestore()") 665 | return stuff 666 | 667 | def multi_command(*cmds): 668 | """ convenience function for setting multiple commands at once in your 669 | .vimpy.rc, like "syntax on", "nohlsearch", etc """ 670 | for cmd in cmds: 671 | command(cmd) 672 | 673 | 674 | class AutoCommandContext(object): 675 | """ an object of this class is passed to functions decorated with one of our 676 | autocommand decorators. its purpose is to give the decorated function 677 | access to buffer-local versions of our helper functions """ 678 | 679 | def abbrev(self, *args, **kwargs): 680 | fn = partial(abbrev, local=True) 681 | return fn(*args, **kwargs) 682 | 683 | def let(self, *args, **kwargs): 684 | fn = partial(let, scope=NS_BUFFER) 685 | return fn(*args, **kwargs) 686 | 687 | def set_option(self, *args, **kwargs): 688 | fn = partial(set_option, local=True) 689 | return fn(*args, **kwargs) 690 | 691 | def visual_key_map(self, *args, **kwargs): 692 | fn = partial(visual_key_map, local=True) 693 | return fn(*args, **kwargs) 694 | 695 | def key_map(self, *args, **kwargs): 696 | fn = partial(key_map, local=True) 697 | return fn(*args, **kwargs) 698 | 699 | 700 | def on_autocmd(event, filetype): 701 | """ A decorator for functions to trigger on AutoCommand events. 702 | Your function will be passed an instance of 703 | AutoCommandContext, which contains on it *buffer-local* methods that would 704 | be useful to you. A filetype of "*" matches all files. 705 | For a list of eligible events, try :help autocmd-events in vim. 706 | """ 707 | def wrapped(fn): 708 | au_name = _generate_autocommand_name(fn) 709 | command("augroup %s" % au_name) 710 | command("autocmd!") 711 | ctx = AutoCommandContext() 712 | call = register_fn(partial(fn, ctx)) 713 | command("autocmd %s %s :%s %s" % (event, filetype, PYTHON_CMD, call)) 714 | command("augroup END") 715 | return fn 716 | 717 | return wrapped 718 | 719 | when_buffer_is = partial(on_autocmd, "FileType") 720 | when_buffer_is.__doc__ = """ A decorator for functions you wish to run when the buffer 721 | filetype=filetype. This is useful if you want to set some keybindings for a 722 | python buffer that you just opened """ 723 | 724 | 725 | def set_filetype(pat, ftype): 726 | s = "au BufRead,BufNewFile {pat} set filetype={ftype}" 727 | s = s.format(pat=pat, ftype=ftype) 728 | command(s) 729 | 730 | 731 | if "snake.plugin_loader" in sys.modules: 732 | plugin_loader = reload(plugin_loader) 733 | else: 734 | from . import plugin_loader 735 | -------------------------------------------------------------------------------- /plugin/snake/plugin_loader.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import sys 3 | import os 4 | from os.path import expanduser, exists, abspath, join, dirname 5 | from contextlib import contextmanager 6 | import subprocess 7 | import logging 8 | import snake 9 | 10 | 11 | log = logging.getLogger("snake.plugins") 12 | 13 | 14 | # virtualenv may not exist, but we also may not need it if the user is just 15 | # running scripts that have no dependencies outside of the stdlib 16 | try: 17 | import pip 18 | except ImportError: 19 | pip = None 20 | 21 | try: 22 | import virtualenv 23 | except ImportError: 24 | virtualenv = None 25 | 26 | # let's use our virtualenv_wrapper home if we have one, else default to 27 | # something sensible. we'll use this to create our new virtualenvs for plugins 28 | # that require them 29 | WORKON_HOME = os.environ.get("WORKON_HOME", "~/.virtualenvs") 30 | VENV_BASE_DIR = abspath(expanduser(WORKON_HOME)) 31 | BUNDLE_DIR = abspath(expanduser("~/.vim/bundle")) 32 | IS_PY3 = sys.version_info[0] == 3 33 | 34 | 35 | if IS_PY3: 36 | def execfile(name, ctx): 37 | exec(open(name).read(), ctx) 38 | 39 | 40 | def venv_exists(plugin_name): 41 | return exists(join(VENV_BASE_DIR, plugin_name)) 42 | 43 | def pip_install(reqs_file, install_dir): 44 | """ takes a requirements file and installs all the reqs in that file into 45 | the virtualenv """ 46 | args = ["pip", "install", "--quiet", "-t", install_dir, "-r", reqs_file] 47 | exit_code = subprocess.call(args) 48 | 49 | # we have to specify --system sometimes on ubuntu, because ubuntu can ship 50 | # with an older pip which defaults to --user, which conflicts with -t. 51 | # --system effectively disables --user (ugly workaround) 52 | if exit_code != 0: 53 | args.append("--system") 54 | exit_code = subprocess.call(args) 55 | 56 | 57 | def venv_name_from_module_name(name): 58 | return "snake_plugin_%s" % name 59 | 60 | def new_venv(name): 61 | home_dir = join(VENV_BASE_DIR, name) 62 | virtualenv.create_environment(home_dir) 63 | return home_dir 64 | 65 | def find_site_packages(venv_dir): 66 | return join(venv_dir, "lib", "python%s" % sys.version[:3], "site-packages") 67 | 68 | class SnakePluginHook(object): 69 | """ allows us to import plugins while installing their dependencies like so: 70 | 71 | from snake.plugins import something 72 | 73 | if "requirements.txt" exists in the directory where the "something" module 74 | lives, they will be installed to the virtualenv for "something" 75 | 76 | https://www.python.org/dev/peps/pep-0302/ 77 | """ 78 | 79 | def __init__(self, plugin_paths): 80 | self.plugin_paths = plugin_paths 81 | 82 | def find_module(self, fullname, path=None): 83 | loader = None 84 | self.parts = fullname.split(".") 85 | self.plugin_module = ".".join(self.parts[-1:]) 86 | 87 | if fullname.startswith("snake.plugins"): 88 | # its our initial snake_plugins dummy module 89 | if len(self.parts) == 2: 90 | loader = self 91 | 92 | # it's a plugin 93 | # notice we're checking for an exact match of 3 parts. like 94 | # "snake.plugins.something". if "something" is a package, we only 95 | # want to use our plugin hook for that top level module. any 96 | # relative imports should be handled by the vanilla plugin system 97 | elif len(self.parts) == 3: 98 | # try to see if it actually is a snake plugin by searching the 99 | # plugin paths. if we can't find it, we'll just end up 100 | # returning None for our loader 101 | try: 102 | imp.find_module(self.plugin_module, self.plugin_paths) 103 | except ImportError: 104 | pass 105 | # we found the plugin 106 | else: 107 | loader = self 108 | 109 | return loader 110 | 111 | def load_module(self, fullname): 112 | mod = None 113 | 114 | # we haven't loaded it, so let's figure out what we're loading 115 | if fullname.startswith("snake.plugins"): 116 | 117 | # its our initial snake_plugins dummy module 118 | if len(self.parts) == 2: 119 | mod = imp.new_module(self.parts[0]) 120 | 121 | mod.__name__ = "snake.plugins" 122 | mod.__loader__ = self 123 | mod.__file__ = "" 124 | mod.__path__ = self.plugin_paths 125 | mod.__package__ = fullname 126 | 127 | # it's a snake plugin 128 | elif len(self.parts) == 3: 129 | plugin_name = self.parts[-1] 130 | h, pathname, desc = imp.find_module(self.plugin_module, 131 | self.plugin_paths) 132 | 133 | is_package = desc[-1] == imp.PKG_DIRECTORY 134 | 135 | # the module is a package, therefore there might be a 136 | # requirements file, and if there's a reqs file, there's a 137 | # virtualenv that we need to activate 138 | if is_package: 139 | venv_name = venv_name_from_module_name(plugin_name) 140 | reqs = join(pathname, "requirements.txt") 141 | 142 | needs_venv = not venv_exists(venv_name) and exists(reqs) 143 | can_make_venv = virtualenv is not None and pip is not None 144 | 145 | # no virtualenv for this plugin? but we have a requirements 146 | # file? create one and install all of the requirements 147 | if needs_venv: 148 | if can_make_venv: 149 | print("Creating virtual environment %s for Snake \ 150 | plugin %s..." % (venv_name, plugin_name)) 151 | venv_dir = new_venv(venv_name) 152 | print("Installing requirements for plugin %s..." % 153 | plugin_name) 154 | pip_install(reqs, find_site_packages(venv_dir)) 155 | else: 156 | raise Exception("Plugin %s requires a virtualenv. \ 157 | Please install virtualenv and pip so that one can be created." % plugin_name) 158 | 159 | # we must have had requirements, because we've created a 160 | # virtualenv. go ahead and evaluate our module inside our new 161 | # venv 162 | if venv_exists(venv_name): 163 | with in_virtualenv(venv_name): 164 | mod = imp.load_module(fullname, h, pathname, desc) 165 | mod.__virtualenv__ = venv_name 166 | 167 | else: 168 | mod = imp.load_module(fullname, h, pathname, desc) 169 | 170 | # we're not a package, there is no virtualenv, so load the 171 | # module as regular 172 | else: 173 | mod = imp.load_module(fullname, h, pathname, desc) 174 | 175 | mod.__loader__ = self 176 | mod.__package__ = fullname 177 | sys.modules[fullname] = mod 178 | 179 | return mod 180 | 181 | 182 | _snake_plugin_paths = [BUNDLE_DIR] 183 | sys.meta_path.insert(0, SnakePluginHook(_snake_plugin_paths)) 184 | 185 | 186 | 187 | @contextmanager 188 | def in_virtualenv(venv_name): 189 | """ activates a virtualenv for the context of the with-block """ 190 | old_path = os.environ["PATH"] 191 | old_sys_path = sys.path[:] 192 | 193 | activate_this = join(VENV_BASE_DIR, venv_name, "bin/activate_this.py") 194 | execfile(activate_this, dict(__file__=activate_this)) 195 | 196 | try: 197 | yield 198 | finally: 199 | os.environ["PATH"] = old_path 200 | sys.prefix = sys.real_prefix 201 | sys.path[:] = old_sys_path 202 | 203 | 204 | def import_source(name, path): 205 | desc = (".py", "U", imp.PY_SOURCE) 206 | h = open(path, desc[1]) 207 | module = imp.load_module(name, h, path, desc) 208 | return module 209 | 210 | 211 | load_vimrc = bool(int(os.environ.get("LOAD_VIMPY", "1"))) 212 | 213 | vimrc_path = expanduser("~/.vimrc.py") 214 | if exists(vimrc_path) and load_vimrc: 215 | import_source("vimrc", vimrc_path) 216 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join, dirname, abspath, exists 3 | import tempfile 4 | import sys 5 | import unittest 6 | import sh 7 | import json 8 | import codecs 9 | import re 10 | 11 | THIS_DIR = dirname(abspath(__file__)) 12 | SNAKE_DIR = join(THIS_DIR, "plugin") 13 | 14 | # as distinguished from the python *vim* is running 15 | TEST_IS_PY3 = sys.version_info[0] == 3 16 | 17 | version_str = sh.vim(version=True).strip() 18 | 19 | VIM_IS_PY3 = "+python3" in version_str 20 | PYTHON_CMD = "python3" if VIM_IS_PY3 else "python" 21 | 22 | 23 | def create_tmp_file(code, prefix="tmp", delete=True): 24 | """ creates a temporary test file that lives on disk, on which we can run 25 | python with sh """ 26 | 27 | py = tempfile.NamedTemporaryFile(prefix=prefix, delete=delete) 28 | if TEST_IS_PY3: 29 | code = bytes(code, "UTF-8") 30 | py.write(code) 31 | py.flush() 32 | # we don't explicitly close, because close will remove the file, and we 33 | # don't want that until the test case is done. so we let the gc close it 34 | # when it goes out of scope 35 | return py 36 | 37 | 38 | VIMRC = create_tmp_file(r""" 39 | let mapleader = "," 40 | set clipboard=unnamed 41 | set t_vb=ERROR 42 | source {SNAKE_DIR}/prelude.vim 43 | {PYTHON_CMD} << EOF 44 | import sys 45 | from os.path import expanduser 46 | sys.path.insert(0, "{SNAKE_DIR}") 47 | import snake 48 | """.format(PYTHON_CMD=PYTHON_CMD, SNAKE_DIR=SNAKE_DIR)) 49 | 50 | 51 | def run_vim(script, input_str=None, vimrc=VIMRC.name, commands=None): 52 | # we can't use a real fifo because it will block on opening, because one 53 | # side will wait for the other side to open before unblocking 54 | fifo = tempfile.NamedTemporaryFile(delete=True, mode="w+b") 55 | 56 | # we do some super ugly stuff and wrap our script in some helpers, namely a 57 | # helper to send output from our snake script to our test 58 | script = """ 59 | {PYTHON_CMD} << EOF 60 | import json 61 | from snake import * 62 | def send(stuff): 63 | with open("{fifo_filename}", "w") as output: 64 | json.dump(stuff, output) 65 | {script} 66 | EOF 67 | """.format(PYTHON_CMD=PYTHON_CMD, script=script, fifo_filename=fifo.name) 68 | script_file = create_tmp_file(script) 69 | input_file = create_tmp_file(input_str or "") 70 | 71 | # use our custom vimrc, use binary mode (dont add newlines automatically), 72 | # and load script_file as our script to run 73 | args = ["-N", "-n", "-i", "NONE", "-u", vimrc, "-S", script_file.name, "-b"] 74 | 75 | 76 | # sometimes we need to specify our own commands, but most times not 77 | if commands is None: 78 | commands = [] 79 | #commands = ["exec 'silent !echo '. errmsg"] 80 | 81 | # save and exit 82 | commands.append("wqa!") 83 | 84 | for command in commands: 85 | args.extend(["-c", command]) 86 | args.append(input_file.name) 87 | 88 | env = os.environ.copy() 89 | env["LOAD_VIMPY"] = "0" 90 | p = sh.vim(*args, _tty_in=True, _env=env) 91 | err = output = p.stdout 92 | 93 | input_file.seek(0) 94 | changed = input_file.read().decode("utf8") 95 | 96 | sent_data = fifo.read() 97 | if sent_data: 98 | output = json.loads(sent_data) 99 | else: 100 | output = None 101 | return changed, output 102 | 103 | 104 | class VimTests(unittest.TestCase): 105 | def setUp(self): 106 | self.sample_text = "The quick brown fox jumps over the lazy dog" 107 | self.sample_block = """ 108 | Hail Mary, full of grace. 109 | The Lord is with thee. 110 | Blessed art thou amongst women, 111 | and blessed is the fruit of thy womb, Jesus. 112 | Holy Mary, Mother of God, 113 | pray for us sinners, 114 | now and at the hour of our death. 115 | Amen. 116 | """.strip() 117 | 118 | 119 | class Tests(VimTests): 120 | 121 | def test_replace_word(self): 122 | script = """ 123 | replace_word("Test") 124 | keys("3w") 125 | replace_word("awesome") 126 | """ 127 | 128 | changed, output = run_vim(script, self.sample_text) 129 | self.assertEqual(changed, "Test quick brown awesome jumps over the lazy dog") 130 | 131 | def test_get_in_quotes(self): 132 | script = r""" 133 | keys("^4w") 134 | double = get_in_quotes() 135 | keys("$2b") 136 | single = get_in_quotes() 137 | send([double, single]) 138 | """ 139 | changed, output = run_vim(script, """something in "double quotes" and \ 140 | something in 'single quotes'""") 141 | self.assertEqual(output, ["double quotes", "single quotes"]) 142 | 143 | 144 | def test_get_word(self): 145 | script = r""" 146 | keys("^5w") 147 | over = get_word() 148 | send(over) 149 | """ 150 | 151 | changed, output = run_vim(script, self.sample_text) 152 | self.assertEqual(output, "over") 153 | 154 | 155 | def test_delete_word(self): 156 | script = r""" 157 | keys("^5w") 158 | delete_word() 159 | """ 160 | changed, output = run_vim(script, self.sample_text) 161 | self.assertEqual(changed, "The quick brown fox jumps the lazy dog") 162 | 163 | 164 | def test_set_buffer_contents(self): 165 | script = r""" 166 | buf = get_current_buffer() 167 | set_buffer_contents(buf, "new stuff") 168 | """ 169 | 170 | changed, output = run_vim(script, self.sample_text) 171 | self.assertEqual(changed, "new stuff") 172 | 173 | def test_abbrev(self): 174 | script = r""" 175 | abbrev("abc", "123") 176 | keys("iabc\") 177 | """ 178 | changed, output = run_vim(script) 179 | self.assertEqual(changed, "123\n") 180 | 181 | def test_abbrev_fn(self): 182 | script = r""" 183 | def create_inc(): 184 | i = [0] 185 | def inc(): 186 | i[0] += 1 187 | return i[0] 188 | return inc 189 | 190 | abbrev("abc", create_inc()) 191 | keys("iabc\ abc\") 192 | """ 193 | changed, output = run_vim(script) 194 | self.assertEqual(changed, "1 2\n") 195 | 196 | 197 | def test_num_lines(self): 198 | script = r""" 199 | send(get_num_lines()) 200 | """ 201 | _, output = run_vim(script, self.sample_block) 202 | self.assertEqual(output, 8) 203 | 204 | def test_last_line(self): 205 | script = r""" 206 | keys("gg") 207 | test1 = is_last_line() 208 | keys("G") 209 | test2 = is_last_line() 210 | send([test1, test2]) 211 | """ 212 | _, output = run_vim(script, self.sample_block) 213 | self.assertFalse(output[0]) 214 | self.assertTrue(output[1]) 215 | 216 | def test_preserve_cursor(self): 217 | script = r""" 218 | keys("gg^w") 219 | with preserve_cursor(): 220 | keys("jj^w") 221 | word1 = get_word() 222 | 223 | word2 = get_word() 224 | send([word1, word2]) 225 | """ 226 | _, output = run_vim(script, self.sample_block) 227 | self.assertEqual(output[0], "art") 228 | self.assertEqual(output[1], "Mary") 229 | 230 | 231 | def test_preserve_mode(self): 232 | script = r""" 233 | mode1 = get_mode() 234 | with preserve_mode(): 235 | keys("v") 236 | mode2 = get_mode() 237 | mode3 = get_mode() 238 | 239 | keys("V") 240 | mode4 = get_mode() 241 | with preserve_mode(): 242 | keys("\") 243 | mode5 = get_mode() 244 | mode6 = get_mode() 245 | 246 | send([mode1, mode2, mode3, mode4, mode5, mode6]) 247 | """ 248 | _, modes = run_vim(script, self.sample_block) 249 | self.assertEqual(modes, ["n", "v", "n", "V", "n", "V"]) 250 | 251 | 252 | def test_search(self): 253 | script = r""" 254 | search("Mary") 255 | word1 = get_word() 256 | pos1 = get_cursor_position() 257 | 258 | search("Mary") 259 | word2 = get_word() 260 | pos2 = get_cursor_position() 261 | 262 | send([(word1, pos1), (word2, pos2)]) 263 | """ 264 | _, output = run_vim(script, self.sample_block) 265 | self.assertEqual(output, [["Mary", [1, 6]], ["Mary", [5, 6]]]) 266 | 267 | 268 | def test_search_double_quote(self): 269 | script = r""" 270 | keys("^") 271 | search('"') 272 | pos = get_cursor_position() 273 | send(pos) 274 | """ 275 | _, output = run_vim(script, "once \"upon a ' time") 276 | self.assertEqual(output, [1, 6]) 277 | 278 | 279 | def test_search_single_quote(self): 280 | script = r""" 281 | keys("^") 282 | search("'") 283 | pos = get_cursor_position() 284 | send(pos) 285 | """ 286 | _, output = run_vim(script, "once \"upon a ' time") 287 | self.assertEqual(output, [1, 14]) 288 | 289 | 290 | def test_filetype(self): 291 | script = r""" 292 | called = 0 293 | @when_buffer_is("text") 294 | def hooks(ctx): 295 | global called 296 | called += 1 297 | 298 | count1 = called 299 | set_option("filetype", "python") 300 | count2 = called 301 | set_option("filetype", "text") 302 | count3 = called 303 | 304 | send([count1, count2, count3]) 305 | """ 306 | _, output = run_vim(script, self.sample_text) 307 | self.assertEqual(output, [0, 0, 1]) 308 | 309 | 310 | def test_current_file(self): 311 | script = r""" 312 | send(get_current_file()) 313 | """ 314 | _, output = run_vim(script) 315 | self.assertEqual(tempfile.gettempdir(), dirname(output)) 316 | 317 | 318 | class VisualTests(VimTests): 319 | def test_cursor_position(self): 320 | script = r""" 321 | data = [] 322 | 323 | keys("gg^") 324 | data.append(get_cursor_position()) 325 | 326 | keys("llj") 327 | data.append(get_cursor_position()) 328 | 329 | send(data) 330 | """ 331 | changed, output = run_vim(script, self.sample_block) 332 | self.assertEqual(output, [[1,1], [2,3]]) 333 | 334 | 335 | def test_cursor_set_pos(self): 336 | script = r""" 337 | keys("gg^l") 338 | p1 = get_cursor_position() 339 | keys("G$") 340 | eof = get_cursor_position() 341 | 342 | set_cursor_position(p1) 343 | p2 = get_cursor_position() 344 | send(p1 == p2 and p1 != eof) 345 | """ 346 | 347 | changed, output = run_vim(script, self.sample_block) 348 | self.assertTrue(output) 349 | 350 | 351 | def test_get_visual_range(self): 352 | script = r""" 353 | keys("ggllvjjl") 354 | keys("\") 355 | pos = get_visual_range() 356 | send([pos, get_mode()]) 357 | """ 358 | changed, output = run_vim(script, self.sample_block) 359 | ((start_row, start_col), (end_row, end_col)), mode = output 360 | self.assertEqual(start_row, 1) 361 | self.assertEqual(start_col, 3) 362 | self.assertEqual(end_row, 3) 363 | self.assertEqual(end_col, 4) 364 | self.assertEqual(mode, "n") 365 | 366 | def test_get_visual_selection(self): 367 | script = r""" 368 | keys("ggllvjjl") 369 | keys("\") 370 | send([get_visual_selection(), get_mode()]) 371 | """ 372 | changed, output = run_vim(script, self.sample_block) 373 | output, mode = output 374 | self.assertEqual(mode, "n") 375 | self.assertEqual(output, "il Mary, full of grace.\nThe Lord is with \ 376 | thee.\nBles") 377 | 378 | def test_replace_visual_selection(self): 379 | script = r""" 380 | keys("wwvee") 381 | replace_visual_selection("awesome dude") 382 | """ 383 | changed, output = run_vim(script, self.sample_text) 384 | self.assertEqual(changed, "The quick awesome dude jumps over the lazy \ 385 | dog") 386 | 387 | 388 | 389 | class KeyMapTests(VimTests): 390 | def test_leader(self): 391 | script = r""" 392 | def side_effect(): 393 | send(True) 394 | 395 | key_map("a", side_effect) 396 | keys("\a") 397 | """ 398 | changed, output = run_vim(script) 399 | self.assertTrue(output) 400 | 401 | def test_key_map_fn(self): 402 | script = r""" 403 | called = 0 404 | def side_effect(): 405 | global called 406 | called += 1 407 | 408 | key_map("a", side_effect) 409 | keys("aaa") 410 | send(called) 411 | """ 412 | changed, output = run_vim(script) 413 | self.assertEqual(output, 3) 414 | 415 | def test_key_map_decorator(self): 416 | script = r""" 417 | called = 0 418 | @key_map("a") 419 | def side_effect(): 420 | global called 421 | called += 1 422 | 423 | keys("aaaa") 424 | send(called) 425 | """ 426 | changed, output = run_vim(script) 427 | self.assertEqual(output, 4) 428 | 429 | def test_visual_key_map(self): 430 | script = r""" 431 | def process(stuff): 432 | send(stuff) 433 | return "really fast" 434 | 435 | visual_key_map("a", process) 436 | keys("Wviwa") 437 | """ 438 | changed, output = run_vim(script, self.sample_text) 439 | self.assertEqual(output, "quick") 440 | self.assertEqual(changed, "The really fast brown fox jumps over the lazy dog") 441 | 442 | 443 | 444 | class OptionsTests(VimTests): 445 | def test_get_set_value(self): 446 | script = r""" 447 | o1 = int(get_option("tw")) 448 | set_option("tw", 80) 449 | o2 = int(get_option("tw")) 450 | send([o1, o2]) 451 | """ 452 | _, output = run_vim(script) 453 | self.assertEqual(output, [0, 80]) 454 | 455 | def test_get_set_flag(self): 456 | script = r""" 457 | o1 = int(get_option("tw")) 458 | set_option("tw", 80) 459 | o2 = int(get_option("tw")) 460 | send([o1, o2]) 461 | """ 462 | _, output = run_vim(script) 463 | self.assertEqual(output, [0, 80]) 464 | 465 | 466 | class VariableTests(VimTests): 467 | def test_let(self): 468 | script = r""" 469 | var_name = "some_var" 470 | orig = get(var_name) 471 | let(var_name, "testing") 472 | new = get(var_name) 473 | send({ 474 | "original": orig, 475 | "new": new, 476 | }) 477 | """ 478 | _, output = run_vim(script) 479 | self.assertEqual(output["original"], None) 480 | self.assertEqual(output["new"], "testing") 481 | 482 | 483 | def test_multi_let(self): 484 | script = r""" 485 | multi_let( 486 | "test", 487 | a="1", 488 | b="2", 489 | c="3" 490 | ) 491 | send({ 492 | "a": get("a", "test"), 493 | "b": get("b", "test"), 494 | "c": get("c", "test"), 495 | }) 496 | """ 497 | _, output = run_vim(script) 498 | self.assertEqual(output["a"], "1") 499 | self.assertEqual(output["b"], "2") 500 | self.assertEqual(output["c"], "3") 501 | 502 | 503 | class RegisterTests(VimTests): 504 | def test_get_set_register(self): 505 | script = r""" 506 | original = get_register("a") 507 | set_register("a", "register a is set") 508 | set_register("b", "register b is set") 509 | new = get_register("a") 510 | send({ 511 | "original": original, 512 | "new": new, 513 | }) 514 | """ 515 | _, output = run_vim(script) 516 | self.assertEqual(output["original"], None) 517 | self.assertEqual(output["new"], "register a is set") 518 | 519 | def test_clear_registers(self): 520 | script = r""" 521 | set_register("a", "register a is set") 522 | set_register("b", "register b is set") 523 | set_register("c", "register c is set") 524 | a = get_register("a") 525 | b = get_register("b") 526 | c = get_register("c") 527 | clear_register("a") 528 | clear_register("b") 529 | new_a = get_register("a") 530 | new_b = get_register("b") 531 | new_c = get_register("c") 532 | send({ 533 | "a": a, 534 | "b": b, 535 | "c": c, 536 | "new_a": new_a, 537 | "new_b": new_b, 538 | "new_c": new_c, 539 | }) 540 | """ 541 | _, output = run_vim(script) 542 | self.assertEqual(output["a"], "register a is set") 543 | self.assertEqual(output["b"], "register b is set") 544 | self.assertEqual(output["c"], "register c is set") 545 | 546 | self.assertEqual(output["new_a"], None) 547 | self.assertEqual(output["new_b"], None) 548 | self.assertEqual(output["new_c"], "register c is set") 549 | 550 | def test_preserve_registers(self): 551 | script = r""" 552 | data = {} 553 | 554 | with preserve_registers("a"): 555 | set_register("a", "123") 556 | 557 | data["preserved_a"] = get_register("a") 558 | 559 | set_register("a", "not preserved") 560 | set_register("b", "i'll be preserved tho") 561 | with preserve_registers("b"): 562 | set_register("a", "123") 563 | set_register("b", "456") 564 | 565 | data["not_preserved_a"] = get_register("a") 566 | data["preserved_b"] = get_register("b") 567 | send(data) 568 | """ 569 | 570 | _, output = run_vim(script) 571 | self.assertEqual(output["preserved_a"], None) 572 | self.assertEqual(output["not_preserved_a"], "123") 573 | self.assertEqual(output["preserved_b"], "i'll be preserved tho") 574 | 575 | 576 | 577 | class BufferTests(VimTests): 578 | def test_new_buffer(self): 579 | script = r""" 580 | buf1 = get_current_buffer() 581 | num1 = get_num_buffers() 582 | n = new_buffer("test") 583 | buf2 = get_current_buffer() 584 | num2 = get_num_buffers() 585 | set_buffer(n) 586 | buf3 = get_current_buffer() 587 | send([buf1, num1, buf2, num2, buf3]) 588 | """ 589 | changed, output = run_vim(script, self.sample_text, commands=["qa!"]) 590 | self.assertEqual(output, [1, 1, 1, 2, 2]) 591 | 592 | def test_get_buffers(self): 593 | script = r""" 594 | new_buffer("test1") 595 | new_buffer("test2") 596 | send(get_buffers()) 597 | """ 598 | changed, output = run_vim(script, self.sample_text, commands=["qa!"]) 599 | del output["1"]["name"] 600 | self.assertDictEqual(output, { 601 | "1": {'flags': {'active': True, 602 | 'alternate': False, 603 | 'current': True, 604 | 'errors': False, 605 | 'hidden': False, 606 | 'modified': False, 607 | 'readonly': False, 608 | 'unlisted': False}, 609 | }, 610 | "2": {'flags': {'active': False, 611 | 'alternate': False, 612 | 'current': False, 613 | 'errors': False, 614 | 'hidden': True, 615 | 'modified': False, 616 | 'readonly': False, 617 | 'unlisted': False}, 618 | 'name': 'test1'}, 619 | "3": {'flags': {'active': False, 620 | 'alternate': True, 621 | 'current': False, 622 | 'errors': False, 623 | 'hidden': True, 624 | 'modified': False, 625 | 'readonly': False, 626 | 'unlisted': False}, 627 | 'name': 'test2'} 628 | }) 629 | 630 | 631 | if __name__ == "__main__": 632 | print(sh.vim(version=True)) 633 | unittest.main(verbosity=2) 634 | --------------------------------------------------------------------------------