├── .gitignore ├── .travis.yml ├── INSTALLATION.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── contrib ├── completion.bash └── completion.fish ├── images ├── demo1.gif └── demo2.gif ├── nvr ├── __init__.py └── nvr.py ├── setup.py └── tests └── test_nvr.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | neovim_remote.egg-info/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: python 4 | 5 | python: 6 | - 3.5 7 | - 3.6 8 | - 3.7 9 | - 3.8 10 | 11 | install: 12 | - pip3 install . 13 | - wget https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.tar.gz 14 | - tar zxf nvim-linux64.tar.gz 15 | 16 | before_script: 17 | - export PATH=$PWD/nvim-linux64/bin:$PATH 18 | 19 | script: 20 | - pytest -v 21 | 22 | jobs: 23 | include: 24 | - stage: Deploy 25 | if: tag =~ v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+ 26 | python: 3.6 27 | install: true 28 | before_script: true 29 | script: true 30 | deploy: 31 | provider: pypi 32 | distributions: "sdist bdist_wheel" 33 | on: 34 | all_branches: true 35 | user: mhinz 36 | password: 37 | secure: "MngOQJaKlbiW2utrRyJ6Dk/9gUCNpiwxl0rV9v2Ke0ZPvr6WVzO35ykvDVTxBaw68lmZ7h+5CcRebts4Ru4hdZI6y4MSqhM2TOEst8z8Dr3wznmpN8XVki2jLhOBf4FFNC0uaHaQ5/uRfbNpn5p/EQ4mXtfQuL/Zq8noizTG0PE6PUHC+8DpazwvXUBKD7hnNolFxGSFC+7ahZZCgbhcLLQt7CAH72OD8zsYGHxSZSk/Vssom34df0Lqgzy5KLdIvCEg6vPD6LpqR4ATGwuE5T5wKdgQYA7h/DrSb87uaVFRzXwSCbELnjPTPIouu7haTAXS5PaBLZWQUJ45DhKrwdHvqud3ECOA5NgZSiBS8YPDXfDFNKx2jBI5Gytol+mnpl+gK16pv3aP1UbVPn/+OYj/ypGMuWIX3v48OGirT4T+xNdVykI35IJK7jJ943xMBS97MJNofMcIx8Bhi91unndDQSoJ222dpjPkn1D4IFhN2OR1lois1JwoV3ZfPx0K9YXRpQGqLSHy9lN1uKQREczQB2A23sr3N8611tgE0eb7cMkX4jgwZsyCkar4E6UcjYylZ+OMquIq4D7/9uZQtx7fR4TmKMEGiDKI3eI28k/sbKbIvB2H6ra++4VPZ0HQwUibpgzUs3I4HGOMQWVAJ8QvrTTcP9lZ9d+JHpi+h9M=" 38 | 39 | notifications: 40 | email: 41 | on_success: never 42 | -------------------------------------------------------------------------------- /INSTALLATION.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## The normal way 4 | 5 | $ pip3 install neovim-remote 6 | 7 | On most systems this will install to `/usr/local/bin`, which is usually in $PATH 8 | already. You're done. 9 | 10 | If you get a _permission denied_ error, e.g. because it tried to install to 11 | `/usr/bin`, **do not** use `sudo` to force it! 12 | 13 | Use this instead: 14 | 15 | $ pip3 install --user neovim-remote 16 | 17 | This will install to `~/.local/bin` (Linux) or `~/Library/Python/3.x/bin` 18 | (macOS) which needs to be added to $PATH. 19 | 20 | This will give you the correct location: 21 | 22 | $ python3 -c 'import site; print(site.USER_BASE)' 23 | 24 | If `nvr` is not in your path and you installed Python with `asdf`, you might need 25 | to reshim: 26 | 27 | $ pip3 install neovim-remote 28 | $ asdf reshim python 29 | 30 | ## From repo 31 | 32 | If you want to test your own changes, it makes sense to put your local repo into 33 | _develop mode_. That way your Python environment will use the repo directly and 34 | you don't have to reinstall the package after each change: 35 | 36 | $ git clone https://github.com/mhinz/neovim-remote 37 | $ cd neovim-remote 38 | $ pip3 install -e . 39 | 40 | Now, `pip3 list` will show the path to the local repo after the package version. 41 | 42 | ## From zip 43 | 44 | Download the [zipped 45 | sources](https://github.com/mhinz/neovim-remote/archive/master.zip) and install 46 | install them with either `pip3 install master.zip` or by running `python3 47 | setup.py install` in the unzipped directory. 48 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Marco Hinz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include tests * 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: install 2 | 3 | install: 4 | python3 setup.py install 5 | 6 | test: 7 | pytest -v tests 8 | 9 | upload: clean 10 | python3 setup.py sdist bdist_wheel 11 | twine upload --verbose dist/* 12 | 13 | clean: 14 | rm -rf build dist neovim_remote.egg-info 15 | 16 | .PHONY: install test upload 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://travis-ci.org/mhinz/neovim-remote.svg?branch=master)](https://travis-ci.org/mhinz/neovim-remote) 2 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/neovim-remote.svg)](https://pypi.python.org/pypi/neovim-remote) 3 | 4 |
5 |

neovim-remote


6 |
7 | 8 | This package provides an executable called **nvr** which solves these cases: 9 | 10 | - Controlling nvim processes from the shell. E.g. opening files in another 11 | terminal window. 12 | - Opening files from within `:terminal` without starting a nested nvim process. 13 | 14 | --- 15 | 16 | - [Installation](#installation) 17 | - [Theory](#theory) 18 | - [First steps](#first-steps) 19 | - [Typical use cases](#typical-use-cases) 20 | - [Demos](#demos) 21 | - [FAQ](#faq) 22 | 23 | --- 24 | 25 | ## Installation 26 | 27 | pip3 install neovim-remote 28 | 29 | If you encounter any issues, e.g. permission denied errors or you can't find the 30 | `nvr` executable, read [INSTALLATION.md](INSTALLATION.md). 31 | 32 | ## Theory 33 | 34 | **Nvim** always starts a server. Get its address with `:echo v:servername`. Or 35 | specify an address at startup: `nvim --listen /tmp/nvimsocket`. 36 | 37 | **nvr** (the client) will use any address given to it via `--servername`, 38 | `$NVIM_LISTEN_ADDRESS` (obsolete in nvim but still supported in nvr), or 39 | defaults to `/tmp/nvimsocket`. 40 | 41 | If the targeted address does not exist, **nvr** starts a new process by running 42 | "nvim". You can change the command by setting `$NVR_CMD`. _(This requires 43 | forking, so it won't work on Windows.)_ 44 | 45 | ## First steps 46 | 47 | Start a nvim process (which acts as a server) in one shell: 48 | 49 | nvim --listen /tmp/nvimsocket 50 | 51 | And do this in another shell: 52 | 53 | ```sh 54 | # nvr uses /tmp/nvimsocket by default, so we're good. 55 | 56 | # Open two files: 57 | nvr --remote file1 file2 58 | 59 | # Send keys to the current buffer: 60 | nvr --remote-send 'iabc' 61 | # Enter insert mode, insert 'abc', and go back to normal mode again. 62 | 63 | # Evaluate any VimL expression, e.g. get the current buffer: 64 | nvr --remote-expr 'bufname("")' 65 | README.md 66 | ``` 67 | 68 |
69 | click here to see all nvr options 70 | 71 | ``` 72 | $ nvr -h 73 | usage: nvr [arguments] 74 | 75 | Remote control Neovim processes. 76 | 77 | If no process is found, a new one will be started. 78 | 79 | $ nvr --remote-send 'iabc' 80 | $ nvr --remote-expr 'map([1,2,3], "v:val + 1")' 81 | 82 | Any arguments not consumed by options will be fed to --remote-silent: 83 | 84 | $ nvr --remote-silent file1 file2 85 | $ nvr file1 file2 86 | 87 | All --remote options take optional commands. 88 | Exception: --remote-expr, --remote-send. 89 | 90 | $ nvr +10 file 91 | $ nvr +'echomsg "foo" | echomsg "bar"' file 92 | $ nvr --remote-tab-wait +'set bufhidden=delete' file 93 | 94 | Open files in a new window from a terminal buffer: 95 | 96 | $ nvr -cc split file1 file2 97 | 98 | Use nvr from git to edit commit messages: 99 | 100 | $ git config --global core.editor 'nvr --remote-wait-silent' 101 | 102 | optional arguments: 103 | -h, --help show this help message and exit 104 | --remote [ [ ...]] 105 | Use :edit to open files. If no process is found, throw 106 | an error and start a new one. 107 | --remote-wait [ [ ...]] 108 | Like --remote, but block until all buffers opened by 109 | this option get deleted or the process exits. 110 | --remote-silent [ [ ...]] 111 | Like --remote, but throw no error if no process is 112 | found. 113 | --remote-wait-silent [ [ ...]] 114 | Combines --remote-wait and --remote-silent. 115 | --remote-tab [ [ ...]] 116 | Like --remote, but use :tabedit. 117 | --remote-tab-wait [ [ ...]] 118 | Like --remote-wait, but use :tabedit. 119 | --remote-tab-silent [ [ ...]] 120 | Like --remote-silent, but use :tabedit. 121 | --remote-tab-wait-silent [ [ ...]] 122 | Like --remote-wait-silent, but use :tabedit. 123 | --remote-send Send key presses. 124 | --remote-expr Evaluate expression and print result in shell. 125 | --servername Set the address to be used. This overrides the default 126 | "/tmp/nvimsocket" and $NVIM_LISTEN_ADDRESS. 127 | --serverlist Print the TCPv4 and Unix domain socket addresses of 128 | all nvim processes. 129 | -cc Execute a command before every other option. 130 | -c Execute a command after every other option. 131 | -d Diff mode. Use :diffthis on all to be opened buffers. 132 | -l Change to previous window via ":wincmd p". 133 | -o [ ...] 134 | Open files via ":split". 135 | -O [ ...] 136 | Open files via ":vsplit". 137 | -p [ ...] 138 | Open files via ":tabedit". 139 | -q Read errorfile into quickfix list and display first 140 | error. 141 | -s Silence "no server found" message. 142 | -t Jump to file and position of given tag. 143 | --nostart If no process is found, do not start a new one. 144 | --version Show the nvr version. 145 | 146 | Development: https://github.com/mhinz/neovim-remote 147 | 148 | Happy hacking! 149 | ``` 150 |
151 | 152 | ## Typical use cases 153 | 154 | - **Open files from within `:terminal` without starting a nested nvim process.** 155 | 156 | Easy-peasy! Just `nvr file`. 157 | 158 | This works without any prior setup, because `$NVIM` is always set for all 159 | children of the nvim process, including `:terminal`, and `nvr` will default 160 | to that address. 161 | 162 | I often work with two windows next to each other. If one contains the 163 | terminal, I can use `nvr -l foo` to open the file in the other window. 164 | 165 | - **Open files always in the same nvim process no matter which terminal you're in.** 166 | 167 | Just `nvr -s` starts a new nvim process with the server address set to 168 | `/tmp/nvimsocket`. 169 | 170 | Now, no matter which terminal you are in, `nvr file` will always work on 171 | that nvim process. That is akin to `emacsclient` from Emacs. 172 | 173 | - **Use nvr in plugins.** 174 | 175 | Some plugins rely on the `--remote` family of options from Vim. Nvim had to 176 | remove those when they switched to outsource a lot of manual code to libuv. 177 | These options are [planned to be added back](https://github.com/neovim/neovim/issues/1750), though. 178 | 179 | In these cases nvr can be used as a drop-in replacement. E.g. 180 | [vimtex](https://github.com/lervag/vimtex) can be configured to use nvr to 181 | jump to a certain file and line: [read](https://github.com/lervag/vimtex/blob/80b96c13fe9edc5261e9be104fe15cf3bdc3173d/doc/vimtex.txt#L1702-L1708). 182 | 183 | - **Use nvr as git editor.** 184 | 185 | Imagine Neovim is set as your default editor via `$VISUAL` or `$EDITOR`. 186 | 187 | Running `git commit` in a regular shell starts a nvim process. But in a 188 | terminal buffer (`:terminal`), a new nvim process starts as well. Now you 189 | have one nvim nested within another. 190 | 191 | If you do not want this, put this in your vimrc: 192 | 193 | ```vim 194 | if has('nvim') 195 | let $GIT_EDITOR = 'nvr -cc split --remote-wait' 196 | endif 197 | ``` 198 | 199 | That way, you get a new window for inserting the commit message instead of a 200 | nested nvim process. But git still waits for nvr to finish, so make sure to 201 | delete the buffer after saving the commit message: `:w | bd`. 202 | 203 | If you don't like using `:w | bd` and prefer the good old `:wq` (or `:x`), 204 | put the following in your vimrc: 205 | 206 | ```vim 207 | autocmd FileType gitcommit,gitrebase,gitconfig set bufhidden=delete 208 | ``` 209 | 210 | To use nvr from a regular shell as well: 211 | 212 | $ git config --global core.editor 'nvr --remote-wait-silent' 213 | 214 | - **Use nvr as git mergetool.** 215 | 216 | If you want to use nvr for `git difftool` and `git mergetool`, put this in 217 | your gitconfig: 218 | 219 | ``` 220 | [diff] 221 | tool = nvr 222 | [difftool "nvr"] 223 | cmd = nvr -s -d $LOCAL $REMOTE 224 | [merge] 225 | tool = nvr 226 | [mergetool "nvr"] 227 | cmd = nvr -s -d $LOCAL $BASE $REMOTE $MERGED -c 'wincmd J | wincmd =' 228 | ``` 229 | 230 | `nvr -d` is a shortcut for `nvr -d -O` and acts like `vim -d`, thus it uses 231 | `:vsplit` to open the buffers. If you want them to be opened via `:split` 232 | instead, use `nvr -d -o`. 233 | 234 | When used as mergetool and all four buffers got opened, the cursor is in the 235 | window containing the $MERGED buffer. We move it to the bottom via `:wincmd 236 | J` and then equalize the size of all windows via `:wincmd =`. 237 | 238 | - **Use nvr for scripting.** 239 | 240 | You might draw some inspiration from [this Reddit 241 | thread](https://www.reddit.com/r/neovim/comments/aex45u/integrating_nvr_and_tmux_to_use_a_single_tmux_per). 242 | 243 | ## Demos 244 | 245 | _(Click on the GIFs to watch them full-size.)_ 246 | 247 | Using nvr from another shell: ![Demo 1](https://github.com/mhinz/neovim-remote/raw/master/images/demo1.gif) 248 | 249 | Using nvr from within `:terminal`: ![Demo 2](https://github.com/mhinz/neovim-remote/raw/master/images/demo2.gif) 250 | 251 | ## FAQ 252 | 253 | - **How to open directories?** 254 | 255 | `:e /tmp` opens a directory view via netrw. Netrw works by hooking into certain 256 | events, `BufEnter` in this case (see `:au FileExplorer` for all of them). 257 | 258 | Unfortunately Neovim's API doesn't trigger any autocmds on its own, so simply 259 | `nvr /tmp` won't work. Meanwhile you can work around it like this: 260 | 261 | $ nvr /tmp -c 'doautocmd BufEnter' 262 | 263 | - **Reading from stdin?** 264 | 265 | Yes! E.g. `echo "foo\nbar" | nvr -o -` and `cat file | nvr --remote -` work just 266 | as you would expect them to work. 267 | 268 | - **Exit code?** 269 | 270 | If you use a [recent enough 271 | Neovim](https://github.com/neovim/neovim/commit/d2e8c76dc22460ddfde80477dd93aab3d5866506), nvr will use the same exit code as the linked nvim. 272 | 273 | E.g. `nvr --remote-wait ` and then `:cquit` in the linked nvim will make 274 | nvr return with 1. 275 | 276 | - **How to send a message to all waiting clients?** 277 | 278 | If you open a buffer with any of the _wait_ options, that buffer will get a 279 | variable `b:nvr`. The variable contains a list of channels wheres each 280 | channel is a waiting nvr client. 281 | 282 | Currently nvr only understands the `Exit` message. You could use it to 283 | disconnect all waiting nvr clients at once: 284 | 285 | ```vim 286 | command! DisconnectClients 287 | \ if exists('b:nvr') 288 | \| for client in b:nvr 289 | \| silent! call rpcnotify(client, 'Exit', 1) 290 | \| endfor 291 | \| endif 292 | ``` 293 | 294 | - **Can I have auto-completion for bash/fish?** 295 | 296 | If you want basic auto-completion for bash, you can source [this 297 | script](contrib/completion.bash) in your .bashrc. 298 | 299 | This also completes server names with the `--servername` option. 300 | 301 | If you want auto-completion for fish, you can add [this 302 | file](contrib/completion.fish) to your fish completions dir. 303 | -------------------------------------------------------------------------------- /contrib/completion.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # bash command completion for neovim remote. 3 | # Source that file in your bashrc to use it. 4 | 5 | _nvr_opts_completions() 6 | { 7 | local cur prev opts 8 | cur=${COMP_WORDS[COMP_CWORD]} 9 | prev=${COMP_WORDS[COMP_CWORD-1]} 10 | opts=( 11 | -h 12 | -cc 13 | -c 14 | -d 15 | -l 16 | -o 17 | -O 18 | -p 19 | -q 20 | -s 21 | -t 22 | --nostart 23 | --version 24 | --serverlist 25 | --servername 26 | --remote 27 | --remote-wait 28 | --remote-silent 29 | --remote-wait-silent 30 | --remote-tab 31 | --remote-tab-wait 32 | --remote-tab-silent 33 | --remote-tab-wait-silent 34 | --remote-send 35 | --remote-expr 36 | ) 37 | case "${prev}" in 38 | --servername) 39 | srvlist=$(nvr --serverlist) 40 | COMPREPLY=( $(compgen -W "${srvlist}" -- "$cur") ) 41 | return 0 42 | ;; 43 | -[oOpq]) 44 | # These options require at least one argument. 45 | COMPREPLY=( $(compgen -A file -- "$cur") ) 46 | return 0 47 | ;; 48 | esac 49 | if [[ "$cur" =~ ^- ]]; then 50 | COMPREPLY=( $(compgen -W "${opts[*]}" -- "$cur") ) 51 | else 52 | COMPREPLY=( $(compgen -A file -- "$cur") ) 53 | fi 54 | return 0 55 | } 56 | 57 | complete -o default -F _nvr_opts_completions nvr 58 | -------------------------------------------------------------------------------- /contrib/completion.fish: -------------------------------------------------------------------------------- 1 | # Completions for nvr in fish shell 2 | # To install, save file to completions directory of fish config (e.g. ~/.config/fish/completions/nvr.fish) 3 | 4 | complete --command=nvr --long-option=remote --description='Use :edit to open files. If no process is found, throw an error and start a new one' 5 | complete --command=nvr --long-option=remote-wait --description='Like --remote, but block until all buffers opened by this option get deleted or the process exits' 6 | complete --command=nvr --long-option=remote-silent --description='Like --remote, but throw no error if no process is found' 7 | complete --command=nvr --long-option=remote-wait-silent --description='Combines --remote-wait and --remote-silent' 8 | complete --command=nvr --long-option=remote-tab --description='Like --remote, but use :tabedit' 9 | complete --command=nvr --long-option=remote-tab-wait --description='Like --remote-wait, but use :tabedit' 10 | complete --command=nvr --long-option=remote-tab-silent --description='Like --remote-silent, but use :tabedit' 11 | complete --command=nvr --long-option=remote-tab-wait-silent --description='Like --remote-wait-silent, but use :tabedit' 12 | complete --command=nvr --long-option=remote-send --no-files --description='Send key presses' 13 | complete --command=nvr --long-option=remote-expr --no-files --description='Evaluate expression and print result in shell' 14 | complete --command=nvr --long-option=servername --no-files --arguments='(nvr --serverlist)' --description='Set the address to be used. This overrides the default "/tmp/nvimsocket" and $NVIM_LISTEN_ADDRESS' 15 | complete --command=nvr --long-option=serverlist --description='Print the TCPv4 and Unix domain socket addresses of all nvim processes' 16 | complete --command=nvr --short-option=h --long-option=help --description='show help message and exit' 17 | complete --command=nvr --short-option=c --no-files --description='Execute a command after every other option' 18 | complete --command=nvr --short-option=d --description='Diff mode. Use :diffthis on all to be opened buffers' 19 | complete --command=nvr --short-option=l --description='Change to previous window via ":wincmd p"' 20 | complete --command=nvr --short-option=o --description='Open files via ":split"' 21 | complete --command=nvr --short-option=O --description='Open files via ":vsplit"' 22 | complete --command=nvr --short-option=p --description='Open files via ":tabedit"' 23 | complete --command=nvr --short-option=q --description='Read errorfile into quickfix list and display first error' 24 | complete --command=nvr --short-option=s --description='Silence "no server found" message' 25 | complete --command=nvr --short-option=t --no-files --description='Jump to file and position of given tag' 26 | complete --command=nvr --long-option=nostart --description='If no process is found, do not start a new one' 27 | complete --command=nvr --long-option=version --description='Show the nvr version' 28 | complete --command=nvr --old-option=cc --description='Execute a command before every other option' 29 | -------------------------------------------------------------------------------- /images/demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhinz/neovim-remote/1004d41696a3de12f0911b1949327c3dbe4a62ab/images/demo1.gif -------------------------------------------------------------------------------- /images/demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhinz/neovim-remote/1004d41696a3de12f0911b1949327c3dbe4a62ab/images/demo2.gif -------------------------------------------------------------------------------- /nvr/__init__.py: -------------------------------------------------------------------------------- 1 | from .nvr import main 2 | -------------------------------------------------------------------------------- /nvr/nvr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Copyright (c) 2015 - present Marco Hinz 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import argparse 26 | import multiprocessing 27 | import os 28 | import re 29 | import sys 30 | import textwrap 31 | import time 32 | import traceback 33 | 34 | import psutil 35 | import pynvim 36 | 37 | class Nvr(): 38 | def __init__(self, address, silent=False): 39 | self.address = address 40 | self.server = None 41 | self.silent = silent 42 | self.wait = 0 43 | self.started_new_process = False 44 | self.handled_first_buffer = False 45 | self.diffmode = False 46 | 47 | def attach(self): 48 | try: 49 | socktype, address, port = parse_address(self.address) 50 | if socktype == 'tcp': 51 | self.server = pynvim.attach('tcp', address=address, port=int(port)) 52 | else: 53 | self.server = pynvim.attach('socket', path=address) 54 | except OSError: 55 | # Ignore invalid addresses. 56 | pass 57 | 58 | def try_attach(self, args, nvr, options, arguments): 59 | for i in range(10): 60 | self.attach() 61 | if self.server: 62 | self.started_new_process = True 63 | return proceed_after_attach(nvr, options, arguments) 64 | time.sleep(0.2) 65 | print(f'[!] Unable to attach to the new nvim process. Is `{" ".join(args)}` working?') 66 | sys.exit(1) 67 | 68 | def execute_new_nvim_process(self, silent, nvr, options, arguments): 69 | if not silent: 70 | print(textwrap.dedent('''\ 71 | [*] Starting new nvim process using $NVR_CMD or 'nvim'. 72 | 73 | Use --nostart to avoid starting a new process. 74 | ''')) 75 | 76 | args = os.environ.get('NVR_CMD') 77 | args = args.split(' ') if args else ['nvim'] 78 | args.extend(['--listen', self.address]) 79 | 80 | multiprocessing.Process(target=self.try_attach, args=(args[0], nvr, options, arguments)).start() 81 | 82 | try: 83 | os.execvpe(args[0], args, os.environ) 84 | except FileNotFoundError: 85 | print(f'[!] Can\'t start new nvim process: `{args[0]}` is not in $PATH.') 86 | sys.exit(1) 87 | 88 | def read_stdin_into_buffer(self, cmd): 89 | self.server.command(cmd) 90 | for line in sys.stdin: 91 | self.server.funcs.append('$', line[:-1]) 92 | self.server.command('silent 1delete _ | set nomodified') 93 | 94 | def fnameescaped_command(self, cmd, path): 95 | if not is_netrw_protocol(path): 96 | path = os.path.abspath(path) 97 | path = self.server.funcs.fnameescape(path) 98 | shortmess = self.server.options['shortmess'] 99 | self.server.options['shortmess'] = shortmess.replace('F', '') 100 | self.server.command(f'{cmd} {path}') 101 | self.server.options['shortmess'] = shortmess 102 | 103 | def diffthis(self): 104 | if self.diffmode: 105 | self.server.command('diffthis') 106 | if not self.started_new_process: 107 | self.wait_for_current_buffer() 108 | 109 | def wait_for_current_buffer(self): 110 | bvars = self.server.current.buffer.vars 111 | chanid = self.server.channel_id 112 | 113 | self.server.command('augroup nvr') 114 | self.server.command(f'autocmd BufDelete silent! call rpcnotify({chanid}, "BufDelete")') 115 | self.server.command(f'autocmd VimLeave * if exists("v:exiting") && v:exiting > 0 | silent! call rpcnotify({chanid}, "Exit", v:exiting) | endif') 116 | self.server.command('augroup END') 117 | 118 | if 'nvr' in bvars: 119 | if chanid not in bvars['nvr']: 120 | bvars['nvr'] = [chanid] + bvars['nvr'] 121 | else: 122 | bvars['nvr'] = [chanid] 123 | 124 | self.wait += 1 125 | 126 | def execute(self, arguments, cmd='edit', silent=False, wait=False): 127 | cmds, files = split_cmds_from_files(arguments) 128 | 129 | for fname in files: 130 | if fname == '-': 131 | self.read_stdin_into_buffer(stdin_cmd(cmd)) 132 | else: 133 | try: 134 | if self.started_new_process and not self.handled_first_buffer: 135 | self.fnameescaped_command('edit', fname) 136 | self.handled_first_buffer = True 137 | else: 138 | self.fnameescaped_command(cmd, fname) 139 | except pynvim.api.nvim.NvimError as e: 140 | if not re.search('E37', e.args[0].decode()): 141 | traceback.print_exc() 142 | sys.exit(1) 143 | self.diffthis() 144 | 145 | if wait: 146 | self.wait_for_current_buffer() 147 | 148 | for cmd in cmds: 149 | self.server.command(cmd if cmd else '$') 150 | 151 | return len(files) 152 | 153 | 154 | def stdin_cmd(cmd): 155 | return { 156 | 'edit': 'enew', 157 | 'split': 'new', 158 | 'vsplit': 'vnew', 159 | 'tabedit': 'tabnew', 160 | }[cmd] 161 | 162 | 163 | def is_netrw_protocol(path): 164 | protocols = [ 165 | re.compile('^davs?://*'), 166 | re.compile('^file://*'), 167 | re.compile('^ftp://*'), 168 | re.compile('^https?://*'), 169 | re.compile('^rcp://*'), 170 | re.compile('^rsync://*'), 171 | re.compile('^scp://*'), 172 | re.compile('^sftp://*'), 173 | ] 174 | 175 | return True if any(prot.match(path) for prot in protocols) else False 176 | 177 | 178 | def parse_args(argv): 179 | form_class = argparse.RawDescriptionHelpFormatter 180 | usage = argv[0] + ' [arguments]' 181 | epilog = 'Development: https://github.com/mhinz/neovim-remote\n\nHappy hacking!' 182 | desc = textwrap.dedent(""" 183 | Remote control Neovim processes. 184 | 185 | If no process is found, a new one will be started. 186 | 187 | $ nvr --remote-send 'iabc' 188 | $ nvr --remote-expr 'map([1,2,3], \"v:val + 1\")' 189 | 190 | Any arguments not consumed by options will be fed to --remote-silent: 191 | 192 | $ nvr --remote-silent file1 file2 193 | $ nvr file1 file2 194 | 195 | All --remote options take optional commands. 196 | Exception: --remote-expr, --remote-send. 197 | 198 | $ nvr +10 file 199 | $ nvr +'echomsg "foo" | echomsg "bar"' file 200 | $ nvr --remote-tab-wait +'set bufhidden=delete' file 201 | 202 | Open files in a new window from a terminal buffer: 203 | 204 | $ nvr -cc split file1 file2 205 | 206 | Use nvr from git to edit commit messages: 207 | 208 | $ git config --global core.editor 'nvr --remote-wait-silent' 209 | """) 210 | 211 | parser = argparse.ArgumentParser( 212 | formatter_class = form_class, 213 | usage = usage, 214 | epilog = epilog, 215 | description = desc) 216 | 217 | parser.add_argument('--remote', 218 | nargs = '*', 219 | metavar = '', 220 | help = 'Use :edit to open files. If no process is found, throw an error and start a new one.') 221 | parser.add_argument('--remote-wait', 222 | nargs = '*', 223 | metavar = '', 224 | help = 'Like --remote, but block until all buffers opened by this option get deleted or the process exits.') 225 | parser.add_argument('--remote-silent', 226 | nargs = '*', 227 | metavar = '', 228 | help = 'Like --remote, but throw no error if no process is found.') 229 | parser.add_argument('--remote-wait-silent', 230 | nargs = '*', 231 | metavar = '', 232 | help = 'Combines --remote-wait and --remote-silent.') 233 | 234 | parser.add_argument('--remote-tab', 235 | nargs = '*', 236 | metavar = '', 237 | help = 'Like --remote, but use :tabedit.') 238 | parser.add_argument('--remote-tab-wait', 239 | nargs = '*', 240 | metavar = '', 241 | help = 'Like --remote-wait, but use :tabedit.') 242 | parser.add_argument('--remote-tab-silent', 243 | nargs = '*', 244 | metavar = '', 245 | help = 'Like --remote-silent, but use :tabedit.') 246 | parser.add_argument('--remote-tab-wait-silent', 247 | nargs = '*', 248 | metavar = '', 249 | help = 'Like --remote-wait-silent, but use :tabedit.') 250 | 251 | parser.add_argument('--remote-send', 252 | metavar = '', 253 | help = 'Send key presses.') 254 | parser.add_argument('--remote-expr', 255 | metavar = '', 256 | help = 'Evaluate expression and print result in shell.') 257 | 258 | parser.add_argument('--servername', 259 | metavar = '', 260 | help = 'Set the address to be used. This overrides the default "/tmp/nvimsocket" and $NVIM_LISTEN_ADDRESS.') 261 | parser.add_argument('--serverlist', 262 | action = 'store_true', 263 | help = 'Print the TCPv4 and Unix domain socket addresses of all nvim processes.') 264 | 265 | parser.add_argument('-cc', 266 | action = 'append', 267 | metavar = '', 268 | help = 'Execute a command before every other option.') 269 | parser.add_argument('-c', 270 | action = 'append', 271 | metavar = '', 272 | help = 'Execute a command after every other option.') 273 | parser.add_argument('-d', 274 | action = 'store_true', 275 | help = 'Diff mode. Use :diffthis on all to be opened buffers.') 276 | parser.add_argument('-l', 277 | action = 'store_true', 278 | help = 'Change to previous window via ":wincmd p".') 279 | parser.add_argument('-o', 280 | nargs = '+', 281 | metavar = '', 282 | help = 'Open files via ":split".') 283 | parser.add_argument('-O', 284 | nargs = '+', 285 | metavar = '', 286 | help = 'Open files via ":vsplit".') 287 | parser.add_argument('-p', 288 | nargs = '+', 289 | metavar = '', 290 | help = 'Open files via ":tabedit".') 291 | parser.add_argument('-q', 292 | metavar = '', 293 | help = 'Read errorfile into quickfix list and display first error.') 294 | parser.add_argument('-s', 295 | action = 'store_true', 296 | help = 'Silence "no server found" message.') 297 | parser.add_argument('-t', 298 | metavar = '', 299 | help = 'Jump to file and position of given tag.') 300 | parser.add_argument('--nostart', 301 | action = 'store_true', 302 | help = 'If no process is found, do not start a new one.') 303 | parser.add_argument('--version', 304 | action = 'store_true', 305 | help = 'Show the nvr version.') 306 | 307 | return parser.parse_known_args(argv[1:]) 308 | 309 | 310 | def show_message(address): 311 | print(textwrap.dedent(f''' 312 | [!] Can't connect to: {address} 313 | 314 | The server (nvim) and client (nvr) have to use the same address. 315 | 316 | Server: 317 | 318 | Specify the server address when starting nvim: 319 | 320 | $ nvim --listen {address} 321 | 322 | Use `:echo v:servername` to verify the address. 323 | 324 | Alternatively, run nvr without arguments. It defaults to 325 | starting a new nvim process with the server name set to 326 | "/tmp/nvimsocket". 327 | 328 | Security: When using an Unix domain socket, that socket should 329 | have proper permissions so that it is only accessible by your 330 | user. 331 | 332 | Client: 333 | 334 | Expose $NVIM_LISTEN_ADDRESS (obsolete in nvim but still 335 | supported by nvr) to the environment before using nvr or use 336 | its --servername option. If neither is given, nvr assumes 337 | \"/tmp/nvimsocket\". 338 | 339 | $ NVIM_LISTEN_ADDRESS={address} nvr file1 file2 340 | $ nvr --servername {address} file1 file2 341 | $ nvr --servername 127.0.0.1:6789 file1 file2 342 | 343 | Use -s to suppress this message. 344 | ''')) 345 | 346 | 347 | def split_cmds_from_files(args): 348 | cmds = [] 349 | files = [] 350 | for _ in range(len(args)): 351 | if args[0][0] == '+': 352 | cmds.append(args.pop(0)[1:]) 353 | elif args[0] == '--': 354 | args.pop(0) 355 | files += args 356 | break 357 | else: 358 | files.append(args.pop(0)) 359 | return cmds, files 360 | 361 | 362 | def print_versions(): 363 | import pkg_resources 364 | print('nvr ' + pkg_resources.require("neovim-remote")[0].version) 365 | print('pynvim ' + pkg_resources.require('pynvim')[0].version) 366 | print('psutil ' + pkg_resources.require('psutil')[0].version) 367 | print('Python ' + sys.version.split('\n')[0]) 368 | 369 | 370 | def print_addresses(): 371 | addresses = [] 372 | errors = [] 373 | 374 | for proc in psutil.process_iter(attrs=['name']): 375 | if proc.info['name'] == 'nvim': 376 | try: 377 | for conn in proc.connections('inet4'): 378 | addresses.insert(0, ':'.join(map(str, conn.laddr))) 379 | for conn in proc.connections('inet6'): 380 | addresses.insert(0, ':'.join(map(str, conn.laddr))) 381 | try: 382 | for conn in proc.connections('unix'): 383 | if conn.laddr: 384 | addresses.insert(0, conn.laddr) 385 | except FileNotFoundError: 386 | # Windows does not support Unix domain sockets and WSL1 387 | # does not implement /proc/net/unix 388 | pass 389 | except psutil.AccessDenied: 390 | errors.insert(0, f'Access denied for nvim ({proc.pid})') 391 | 392 | for addr in sorted(addresses): 393 | print(addr) 394 | for error in sorted(errors): 395 | print(error, file=sys.stderr) 396 | 397 | 398 | def parse_address(address): 399 | try: 400 | host, port = address.rsplit(':', 1) 401 | if port.isdigit(): 402 | return 'tcp', host, port 403 | raise ValueError 404 | except ValueError: 405 | return 'socket', address, None 406 | 407 | 408 | def main(argv=sys.argv, env=os.environ): 409 | options, arguments = parse_args(argv) 410 | 411 | if options.version: 412 | print_versions() 413 | return 414 | 415 | if options.serverlist: 416 | print_addresses() 417 | return 418 | 419 | address = options.servername or env.get('NVIM') or env.get('NVIM_LISTEN_ADDRESS') 420 | if not address: 421 | # Since before build 17063 windows doesn't support unix socket, we need another way 422 | address = '127.0.0.1:6789' if os.name == 'nt' else '/tmp/nvimsocket' 423 | 424 | nvr = Nvr(address, options.s) 425 | nvr.attach() 426 | 427 | if not nvr.server: 428 | if os.path.exists(nvr.address): 429 | print(textwrap.dedent(f''' 430 | [!] A file {nvr.address} exists, but we failed to attach to it. 431 | 432 | Is it a Unix domain socket? Either remove that file and try 433 | again or choose another address with --servername. 434 | ''')) 435 | return 436 | 437 | silent = options.remote_silent or options.remote_wait_silent or options.remote_tab_silent or options.remote_tab_wait_silent or options.s 438 | if not silent: 439 | show_message(address) 440 | if options.nostart: 441 | sys.exit(1) 442 | nvr.execute_new_nvim_process(silent, nvr, options, arguments) 443 | 444 | proceed_after_attach(nvr, options, arguments) 445 | 446 | 447 | def proceed_after_attach(nvr, options, arguments): 448 | if options.d: 449 | nvr.diffmode = True 450 | 451 | if options.cc: 452 | for cmd in options.cc: 453 | if cmd == '-': 454 | cmd = sys.stdin.read() 455 | nvr.server.command(cmd) 456 | 457 | if options.l: 458 | nvr.server.command('wincmd p') 459 | 460 | if options.remote is not None: 461 | nvr.execute(options.remote + arguments, 'edit') 462 | elif options.remote_wait is not None: 463 | nvr.execute(options.remote_wait + arguments, 'edit', wait=True) 464 | elif options.remote_silent is not None: 465 | nvr.execute(options.remote_silent + arguments, 'edit', silent=True) 466 | elif options.remote_wait_silent is not None: 467 | nvr.execute(options.remote_wait_silent + arguments, 'edit', silent=True, wait=True) 468 | elif options.remote_tab is not None: 469 | nvr.execute(options.remote_tab + arguments, 'tabedit') 470 | elif options.remote_tab_wait is not None: 471 | nvr.execute(options.remote_tab_wait + arguments, 'tabedit', wait=True) 472 | elif options.remote_tab_silent is not None: 473 | nvr.execute(options.remote_tab_silent + arguments, 'tabedit', silent=True) 474 | elif options.remote_tab_wait_silent is not None: 475 | nvr.execute(options.remote_tab_wait_silent + arguments, 'tabedit', silent=True, wait=True) 476 | elif arguments and options.d: 477 | # Emulate `vim -d`. 478 | options.O = arguments 479 | arguments = [] 480 | 481 | if options.remote_send: 482 | nvr.server.input(options.remote_send) 483 | 484 | if options.remote_expr: 485 | result = '' 486 | if options.remote_expr == '-': 487 | options.remote_expr = sys.stdin.read() 488 | try: 489 | result = nvr.server.eval(options.remote_expr) 490 | except: 491 | print(textwrap.dedent(f""" 492 | No valid expression: {options.remote_expr} 493 | Test it in Neovim: :echo eval('...') 494 | If you want to execute a command, use -c or -cc instead. 495 | """)) 496 | if type(result) is bytes: 497 | print(result.decode()) 498 | elif type(result) is list: 499 | print(list(map(lambda x: x.decode() if type(x) is bytes else x, result))) 500 | elif type(result) is dict: 501 | print({ (k.decode() if type(k) is bytes else k): v for (k,v) in result.items() }) 502 | else: 503 | result = str(result) 504 | if not result.endswith(os.linesep): 505 | result += os.linesep 506 | print(result, end='', flush=True) 507 | 508 | if options.o: 509 | args = options.o + arguments 510 | if nvr.diffmode and not nvr.started_new_process: 511 | nvr.execute(args[:1], 'tabedit', silent=True, wait=False) 512 | nvr.execute(args[1:], 'split', silent=True, wait=False) 513 | else: 514 | nvr.execute(args, 'split', silent=True, wait=False) 515 | nvr.server.command('wincmd =') 516 | elif options.O: 517 | args = options.O + arguments 518 | if nvr.diffmode and not nvr.started_new_process: 519 | nvr.execute(args[:1], 'tabedit', silent=True, wait=False) 520 | nvr.execute(args[1:], 'vsplit', silent=True, wait=False) 521 | else: 522 | nvr.execute(args, 'vsplit', silent=True, wait=False) 523 | nvr.server.command('wincmd =') 524 | elif options.p: 525 | nvr.execute(options.p + arguments, 'tabedit', silent=True, wait=False) 526 | else: 527 | # Act like --remote-silent by default. 528 | nvr.execute(arguments, 'edit', silent=True) 529 | 530 | if options.t: 531 | try: 532 | nvr.server.command('tag ' + options.t) 533 | except nvr.server.error as e: 534 | print(e) 535 | sys.exit(1) 536 | 537 | if options.q: 538 | path = nvr.server.funcs.fnameescape(os.environ['PWD']) 539 | nvr.server.command('lcd ' + path) 540 | nvr.server.funcs.setqflist([]) 541 | if options.q == '-': 542 | for line in sys.stdin: 543 | nvr.server.command("caddexpr '{}'". 544 | format(line.rstrip().replace("'", "''").replace('|', r'\|'))) 545 | else: 546 | with open(options.q, 'r') as f: 547 | for line in f.readlines(): 548 | nvr.server.command("caddexpr '{}'". 549 | format(line.rstrip().replace("'", "''").replace('|', r'\|'))) 550 | nvr.server.command('silent lcd -') 551 | nvr.server.command('cfirst') 552 | 553 | if options.c: 554 | for cmd in options.c: 555 | if cmd == '-': 556 | cmd = sys.stdin.read() 557 | nvr.server.command(cmd) 558 | 559 | wait_for_n_buffers = nvr.wait 560 | if wait_for_n_buffers > 0: 561 | exitcode = 0 562 | 563 | def notification_cb(msg, args): 564 | nonlocal wait_for_n_buffers 565 | nonlocal exitcode 566 | 567 | if msg == 'BufDelete': 568 | wait_for_n_buffers -= 1 569 | if wait_for_n_buffers == 0: 570 | nvr.server.stop_loop() 571 | exitcode = 0 if len(args) == 0 else args[0] 572 | elif msg == 'Exit': 573 | nvr.server.stop_loop() 574 | exitcode = args[0] 575 | 576 | def err_cb(error): 577 | nonlocal exitcode 578 | print(error, file=sys.stderr) 579 | nvr.server.stop_loop() 580 | exitcode = 1 581 | 582 | nvr.server.run_loop(None, notification_cb, None, err_cb) 583 | nvr.server.close() 584 | sys.exit(exitcode) 585 | 586 | nvr.server.close() 587 | 588 | 589 | if __name__ == '__main__': 590 | main() 591 | 592 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from os import path 3 | 4 | here = path.abspath(path.dirname(__file__)) 5 | 6 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 7 | long_description = f.read() 8 | 9 | setup( 10 | name = 'neovim-remote', 11 | author = 'Marco Hinz', 12 | author_email = 'mh.codebro@gmail.com', 13 | url = 'https://github.com/mhinz/neovim-remote', 14 | description = 'Control nvim processes using "nvr" commandline tool', 15 | long_description = long_description, 16 | long_description_content_type = 'text/markdown', 17 | python_requires = '>=3.7', 18 | install_requires = ['pynvim', 'psutil', 'setuptools'], 19 | entry_points = { 20 | 'console_scripts': ['nvr = nvr.nvr:main'] 21 | }, 22 | packages = ['nvr'], 23 | version = '2.5.1', 24 | license = 'MIT', 25 | keywords = 'neovim nvim nvr remote helper', 26 | classifiers = [ 27 | 'Topic :: Utilities', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Development Status :: 5 - Production/Stable', 30 | 'Intended Audience :: End Users/Desktop', 31 | 'Intended Audience :: Developers', 32 | 'Programming Language :: Python :: 3', 33 | ], 34 | ) 35 | 36 | -------------------------------------------------------------------------------- /tests/test_nvr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import time 5 | import subprocess 6 | import uuid 7 | import nvr 8 | 9 | # Helper functions 10 | 11 | def run_nvim(env): 12 | nvim = subprocess.Popen(['nvim', '-nu', 'NORC', '--headless'], env=env) 13 | time.sleep(1) 14 | return nvim 15 | 16 | def run_nvr(cmdlines, env): 17 | for cmdline in cmdlines: 18 | nvr.main(cmdline, env) 19 | 20 | def setup_env(): 21 | env = {'NVIM_LISTEN_ADDRESS': 'pytest_socket_{}'.format(uuid.uuid4())} 22 | env.update(os.environ) 23 | return env 24 | 25 | # Tests 26 | 27 | def test_remote_send(capsys): 28 | env = setup_env() 29 | nvim = run_nvim(env) 30 | cmdlines = [['nvr', '--nostart', '--remote-send', 'iabc'], 31 | ['nvr', '--nostart', '--remote-expr', 'getline(1)']] 32 | run_nvr(cmdlines, env) 33 | nvim.terminate() 34 | out, err = capsys.readouterr() 35 | assert out == 'abc\n' 36 | 37 | # https://github.com/mhinz/neovim-remote/issues/77 38 | def test_escape_filenames_properly(capsys): 39 | filename = 'a b|c' 40 | env = setup_env() 41 | nvim = run_nvim(env) 42 | cmdlines = [['nvr', '-s', '--nostart', '-o', filename], 43 | ['nvr', '-s', '--nostart', '--remote-expr', 'fnamemodify(bufname(""), ":t")']] 44 | run_nvr(cmdlines, env) 45 | nvim.terminate() 46 | out, err = capsys.readouterr() 47 | assert filename == out.rstrip() 48 | 49 | def test_escape_single_quotes_in_filenames(capsys): 50 | filename = "foo'bar'quux" 51 | env = setup_env() 52 | nvim = run_nvim(env) 53 | cmdlines = [['nvr', '-s', '--nostart', '-o', filename], 54 | ['nvr', '-s', '--nostart', '--remote-expr', 'fnamemodify(bufname(""), ":t")']] 55 | run_nvr(cmdlines, env) 56 | nvim.terminate() 57 | out, err = capsys.readouterr() 58 | assert filename == out.rstrip() 59 | 60 | def test_escape_double_quotes_in_filenames(capsys): 61 | filename = 'foo"bar"quux' 62 | env = setup_env() 63 | nvim = run_nvim(env) 64 | cmdlines = [['nvr', '-s', '--nostart', '-o', filename], 65 | ['nvr', '-s', '--nostart', '--remote-expr', 'fnamemodify(bufname(""), ":t")']] 66 | run_nvr(cmdlines, env) 67 | nvim.terminate() 68 | out, err = capsys.readouterr() 69 | assert filename == out.rstrip() 70 | --------------------------------------------------------------------------------