The Vim editor can be used as a
terminal multiplexer, as a replacement for GNU Screen for example:
run the editor in full screen, create new editing “windows” (Vim
windows, not X windows) as needed with :new
,
:split
or similar commands, and create terminal windows
with :terminal
. Given that I spend most of my time in Vim
anyway, I’d rather have a single instance than fire up a new instance
every time I want to edit a file.
The only issue with running a terminal (or several terminals, from
that matter) from within Vim is when I want to open a file from the
terminal (i.e. by calling vim path/to/file
) instead of
from the editor itself (e.g. with the :edit
command):
calling vim
from the terminal will naturally result in the
shell starting a new instance of Vim in its window, while what I would
like is for the file to be opened in one of the neighbouring editing
windows of the same Vim instance as the one running the terminal.
There’s at least two different ways to achieve that. One is to use the JSON API, which allows you to send to Vim some JSON commands through an escape sequence specifically recognized by the Vim terminal. The other is to use the client-server mechanism.
When inside the Vim terminal, you can ask Vim to open a file with a command like the following:
$ echo -e "\e]51;[\"drop\", \"filename\"]\07"
where:
\e]51;
is the escape sequence specifically recognized
by the Vim terminal (\e
is understood by the
echo
command as representing the escape
character);["drop", "filename"]
is a JSON array that is sent to
Vim and that contains a command to execute and its argument(s)
(here, the :drop
command will open the indicated
file);\07
marks the end of the sequence sent to Vim.It is then easy to wrap the above echo
command into a
script. The script can even detect whether we are inside a Vim terminal
or not, by looking at the VIM_TERMINAL environment variable,
and either emit the escape sequence or start a new instance of Vim. The
same script can then always be used to edit a file, regardless of
whether we are inside a Vim terminal or within any other type of
terminal:
#!/bin/bash if [ -n "$VIM_TERMINAL" ]; then filename=$(realpath $1) echo -e "\e]51;[\"drop\", \"$filename\"]\07" else vim $1 fi
The :drop
command will open a new editing window to edit
the file. What if we want to file to appear in one of the already
existing windows instead? There is no Vim command to open a file in a
specific window, but we can easily add one in the .vimrc
file.
We want that function to be callable by the terminal JSON API, so its
name must start with Tapi_
, and it must take two
arguments: one will be the number of the buffer where the terminal is
running (we won’t use it), the other will be the argument(s) passed in
the JSON array.
function Tapi_Open(bufnum, arglist) if len(a:arglist) == 1 " Called with one argument only, open in a new window. execute "drop" a:arglist[0] elseif len(a:arglist) == 2 " The second argument is the number of the window " where we want to open the file. " First we move to that window execute a:arglist[1] . "wincmd w" " Then we open the file there. execute "edit" a:arglist[0] endif endfunc
The script above can then be modified to accept an optional argument
indicating the window where the file should be opened. If that option is
used, then we call our newly defined function instead of the standard
:drop
command.
#!/bin/bash loop=1 while [ $loop = 1 ]; do case "$1" in -w[1-9]) window=${1#-w} shift 1 ;; -w) window=$2 shift 2 ;; *) loop=0 ;; esac done if [ -n "$VIM_TERMINAL" ]; then filename=$(realpath $1) if [ -n "$window" ]; then echo -e "\e]51;[\"call\", \"Tapi_Open\", [\"$filename\", $window]]\07" else echo -e "\e]51;[\"drop\", \"$filename\"]\07" fi else vim $1 fi
Graphical versions of Vim automatically register themselves as a command server when they are invoked, so there’s nothing special to do. This includes MacVim, despite Vim’s documentation which suggests that the whole client-server feature is only available on X11 and Win32.
The name of the server can be explicitly specified using the
--servername
option. By default Vim will generate a server
name automatically.
The console version of Vim can also act as a command server
if it has been compiled with X support.1 If
it has also been compiled with the --enable-autoservername
option, then it behaves like the graphical version, in that it
automatically register itself as a command server at startup. Without
the autoservername feature, the console version of Vim only
register itself as a command server if the user explicitly requests it
by calling Vim with the --servername
option.
Assuming Vim has registered itself has a command server (whether
automatically or upon explicit request via --servername
),
once we are in a terminal window within the Vim instance we need to get
the name of the Vim server.
That’s easy, since Vim exports the name in the environment variable VIM_SERVERNAME. We can then use that name to send a request to Vim, as in the following example:
$ vim --servername $VIM_SERVERNAME --remote path/to/file
This will open the specified file into a new editing window of the same instance as the one in which we are running the terminal.
Note the dual meaning of the --servername
option. Used
alone, it tells Vim to register as a command server under the
specified name; used in combination with the --remote
option, it instructs Vim to act as a client and to contact
the specified server.
We can then start devising a script similar to the one we wrote above for the terminal API:
#!/bin/bash if [ -n "$VIM_TERMINAL" ]; then vim --servername $VIM_SERVERNAME --remote "$1" else vim "$1" fi
Again, what if we want to open a file in an existing window
instead of creating a new one? Then we can use the
--remote-send
command line option instead of
--remote
, to send Vim an arbitrary key sequence.
For example, the following command will open the file in the 3rd window (if possible: it will fail if the buffer behind that window has unsaved changes):
$ vim --servername $VIM_SERVERNAME --remote-send "<C-\><C-N>3<C-w>w:e filename<CR>"
Decomposing that sequence, we have:
<C-\><C-N>
: ensure we are in normal
mode;3<C-w>w
: move to window number 3;:e filename<CR>
: edit filename.Interestingly, contrary to the method using the terminal API above, this does not require any custom function in Vim’s configuration.
Here is the final script that I use to open files in Vim. It supports both the terminal API and the client-server mechanism.
#!/bin/bash open_server() { # Open a file using the client-server mechanism local servername=$1 local filename=$2 local window=$3 if [ -z "$window" ]; then # No window number, open in a new window vim --servername $servername --remote "$filename" else # Open in the specified window vim --servername $servername --remote-send "<C-\><C-N>$window<C-w>w:e $filename<CR>" fi } open_tapi() { # Open a file using the terminal API local filename=$1 local window=$2 if [ -z "$window" ]; then # No window number, open in a new window echo -e "\e]51;[\"drop\", \"$filename\"]\07" else # Open in the specified window # (assuming a Tapi_Open function has been defined) echo -e "\e]51;[\"call\", \"Tapi_Open\", [\"$filename\", $window]]\07" fi } loop=1 window= if [ -n "$VIM_TERMINAL" ]; then # Running in a terminal, try to get a server name servername=$VIM_SERVERNAME fi while [ $loop = 1 ]; do case "$1" in -r) # Bonus: allow the user to specify the server name herself # (to open a file in a specific Vim instance from any terminal, # not only from inside a Vim terminal) servername=$2 shift 2 ;; -w[1-9]) window=${1#-w} shift 1 ;; -w) window=$2 shift 2 ;; *) loop=0 ;; esac done filename=$(realpath "$1") if [ -n "$servername" ]; then # Use the client-server mechanism if we have a server name open_server $servername "$filename" $window elif [ -n "$VIM_TERMINAL" ]; then # Otherwise use the terminal API open_tapi "$filename" $window else # Not in a Vim terminal, start a new instance normally vim "$filename" fi
Vim windows are numbered predictably: the first window (1) is always the one in the top-left corner, and the following windows are numbered going from top to bottom and then from left to right.
If more than two or three windows are visible, though, it can be nice
to have Vim explicitly show the window numbers, so that we don’t have to
count windows to find the number to pass to the -w
option
of the script above.
This is easily done by setting the stl
(status line)
variable to a custom value in Vim’s configuration, as follows:
set stl=%<[%{winnr()}]\ %f\ %h%m%r%=%-14.(%l,%c%V%)\ %P
The winnr()
function returns the number of the current
window.
--with-x
.