Blog

How to open a file from the Vim terminal

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.

Using the JSON API

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:

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

Using the client-server mechanism

Starting Vim as a command server

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.

Contacting the Vim server from the terminal

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:

Interestingly, contrary to the method using the terminal API above, this does not require any custom function in Vim’s configuration.

Wrapping everything together

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

Aside: Displaying window numbers

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.

  1. This is unfortunately not the case of Slackware’s ap/vim package, which needs to be recompiled with --with-x.