2020-09-26
programming
vim
tmux
Executing code chunks from Vim
Recently “notebook” applications like Jupyter and RStudio have become a popular
way of teaching and practicing programming. Their advantage is making
programming interactive: you write several lines of code, then execute it and
look what happens. You can tinker with your complex data structures and try
unfamiliar functions out without the need to rerun the whole script each time.
However, this also comes with the massive disadvantage: you are locked down to
a GUI application and cannot edit the code with a decent text editor. In this
tutorial, I show how to achieve the same effect using Vim, the standard REPL
console, and tmux to connect them.
Note: tmux or “terminal multiplexer” is a program that allows you to
manage multiple terminal emulators inside one text terminal or window. In
the tutorial below, I assume that you are familiar with the basic concepts
of tmux, like sessions, windows and panes. If not, I strongly recommend
to check out this amazing piece of software.
Before we get started, here’s a short demo of what we’re aiming at:

You can see that the first time I hit Ctrl+E inside Vim, a new tmux window
pops up, showing a session named repl-R and running the R interpreter. Then I
can select chunks of code and press Ctrl+E again, which sends the selected
code to the interpreter. Inside a tiling window manager (mine is
dwm), it looks almost like an RStudio session,
except that I’m using my favourite terminal-based tools. Furthermore, it be
done exactly the same way for different text editors (e.g. Emacs or
vis) and different interpreters (Python,
Haskell, what have you). Let’s get down to implementing it.
Step 1. Sending keys to a tmux pane from outside
In the first step, we’re going to learn how to control an application running
in a tmux pane by running shell commands from outside that pane.
Execute the following steps:
- Start a new tmux session called “test” by running
in a terminal emulator. This will create the session and a new window with
ID 0 inside it.
- Open a completely new terminal window (even without running tmux inside,
doesn’t matter) and execute the following command:
tmux send-keys -t test:0 ls Enter
- Go back to the window running the
test session. You should see that the
ls command has been run in window 0!
As the name suggests, the command send-keys sends keypress events to the tmux
pane indicated by the -t parameter. This way we can control any application
that’s running there from outside.
Now let’s modify those steps slightly to get closer to the goal. In step one,
instead run:
This will open the R interpreter in the newly created window. In step two, run:
tmux send-keys -t test:0 'data(iris)' Enter 'hist(Sepal.Width)' Enter
You should see a nice histogram pop up!
Step 2. Sending the Vim selection
We now know how to send keys to an application running inside a tmux pane with
a shell command. Now we will learn how to run this command from Vim and pass
the currently selected text as parameter.
Vim allows you to run an external command and pass the currently selected lines
to its standard input using the :write command. You simply pass the command
preceded with a bang sign (!) instead of a filename. Try it by selecting some
text in Vim (in visual mode) and running:
A new dialog window should pop up, in which you will see the same text with all
a’s replaced with b’s. This is the output of the command.
Our goal is to call the tmux send-keys ... command on the selected block of
code (say, R code for this example). However, we need to preprocess the code a
little bit to ensure that:
- each line is followed by
Enter,
- the whitespaces, quotes and other special characters are passed correctly,
instead of breaking the shell command.
Below is the function that does the whole work. You can first add it to your
.vimrc and test it a little bit. Then we will discuss it in detail:
function! SendToREPL()
silent! execute '''<,''>write
\ !sed "s/.*/\0\nEnter/"
\ | tr "\n" "\0"
\ | xargs -0 tmux send-keys -t ' . b:repl_session . ':0.0'
endfunction
In order to test it, you need to set the variable b:repl_session to the name
of the tmux session, in which our R interpreter is running:
:let b:repl_session="test"
Then you can call the function by selecting some R code in visual mode and
running:
Note that when you press : in visual mode, the prompt that appears looks like
this: :'<,'> - i.e. it contains the selected range. You need to remove those
extra characters (up to the colon) to achieve the proper behaviour. We will
automatize this in Step 3.
Let’s have a closer look at SendToREPL(). The first line: silent! execute
'''<''>write just executes Vim’s write command on the currently selected
range. silent! means that we don’t want the dialog window with the output to
pop up (because we’re not expecting any output) and the whole command is
enclosed in single quotes ('), with quotes inside the command being escaped
by doubling them.
The three subsequent lines contain the shell command that is passed to write.
It’s actually a pipeline of three commands:
sed "s/.*/\0\nEnter/" adds after each line a line consisting of Enter,
tr "\n" "\0" changes all line breaks to the NUL character - this is
important for the next command,
xargs -0 tmux send-keys -t ' . b:repl_session .':0.0' calls the tmux
send-keys command while passing the lines from its stdin as arguments. The
-0 parameter means that the input lines are separated by NUL characters
instead of newlines - this way they are passed literally, no matter what
whitespace and special characters they contain. As the target tmux pane, we
set the pane 0 of window 0 of the session, the name of which is written in
the variable b:repl_session.
Step 3. Ensuring that the interpreter is running
The missing part is now the first Ctrl+E press, which starts the tmux session
running the interpreter. In order to simplify this process, we will set some
variables based on the file type:
autocmd FileType r,rmd let b:repl_cmd = "R"
autocmd FileType python let b:repl_cmd = "python3"
autocmd FileType r,rmd,python let b:repl_session = "repl-" . b:repl_cmd
The b: prefix means that the variables are local to the buffer. We set two
variables: b:repl_cmd is the command to run the appropriate interpreter,
while b:repl_session is the name of the tmux session where it’s supposed to
be running.
Now we define the following function which starts the interpreter if it’s not
already running:
function! EnsureREPL()
if exists("b:repl_cmd")
call system("tmux has-session -t " . b:repl_session .
\ " || ( st -e tmux new-session -s " .
\ b:repl_session . " " . b:repl_cmd . " ) &")
endif
endfunction
The tmux has-session command checks whether the session is already running.
If not, we start it in a new terminal window - remember to replace st in line
4 with your terminal emulator of choice.
Finally, we set the Ctrl+E keybinding for both normal and visual mode:
autocmd FileType r,rmd,python
\ nnoremap <C-e> :silent! call EnsureREPL()<CR>
autocmd FileType r,rmd,python
\ vnoremap <C-e> :<C-u>silent! call SendToREPL()<CR>
As I’ve noted at the beginning, the workflow is not Vim-specific and can be
implemented in any text editor that allows selecting a chunk of text and piping
it to an external shell command. The essential part here is tmux and this is
one example for how powerful this application is when it comes to managing text
terminals.
Some might say that there are editors that allow you to run a terminal inside
an editor buffer, like neovim. I find this solution somehow inelegant - you
typically run a text editor inside a terminal, so you shouldn’t be running
another terminal inside the editor. Here we use the editor for editing text and
delegate everything else to external applications.
Using this solution for R, I completely eliminated RStudio from my workflow.
I don’t miss its constant crashes at all!
|