Vim follows the UNIX philosophy of "do one thing well". Instead of trying to cram all the functionality you could ever want inside the editor itself, the right way to use Vim is to delegate to external commands when appropriate.
Let's add some interaction with the Potion compiler to our plugin to get our feet wet with external commands in Vim.
First we'll add a command to compile and run the current Potion file. There are a number of ways to do this, but we'll simply use an external command for now.
Create a potion/ftplugin/potion/running.vim
file in your plugin's repo. This
is where we'll create the mappings for compiling and running Potion files.
if !exists("g:potion_command")
let g:potion_command = "potion"
endif
function! PotionCompileAndRunFile()
silent !clear
execute "!" . g:potion_command . " " . bufname("%")
endfunction
nnoremap <buffer> <localleader>r :call PotionCompileAndRunFile()<cr>
The first chunk stores the command used to execute Potion in a global variable, if that variable isn't already set. We've seen this kind of check before.
This will allow users to override it if potion
isn't in their $PATH
by
putting a line like let g:potion_command = "/Users/sjl/src/potion/potion"
in
their ~/.vimrc
file.
The last line adds a buffer-local mapping that calls a function we've defined
above. Remember that because this file is in the ftdetect/potion
directory it
will be run every time a file's filetype
is set to potion
.
The real functionality is in the PotionCompileAndRunFile()
function. Go ahead
and save this file, open up factorial.pn
and press <localleader>r
to run the
mapping and see what happens.
If potion
is in your $PATH
, the file should be run and you should see its
output in your terminal (or at the bottom of the window if you're using a GUI
Vim). If you get an error about the potion
command not being found, you'll
need to set g:potion_command
in your ~/.vimrc
file as mentioned above.
Let's take a look at how PotionCompileAndRunFile()
function works.
The :!
command (pronounced "bang") in Vim runs external commands and displays
their output on the screen. Try it out by running the following command:
:!ls
Vim should show you the output of the ls
command, as well as a "Press ENTER or
type command to continue" prompt.
Vim doesn't pass any input to the command when run this way. Confirm this by running:
:!cat
Type a few lines and you'll see that the cat
command spits them back out, just
as it normally would if you ran cat
outside of Vim. Use Ctrl-D to finish.
To run an external command without the Press ENTER or type command to continue
prompt, use :silent !
. Run the following command:
:silent !echo Hello, world.
If you run this in a GUI Vim like MacVim or gVim, you won't see the Hello,
world.
output of the command.
If you run it in a terminal Vim, your results may vary depending on your
configuration. You may need to run :redraw!
to fix your screen after running
a bare :silent !
.
Note that this command is :silent !
and not :silent!
(see the space?)!
Those are two different commands, and we want the former! Isn't Vimscript
great?
Let's look back at the PotionCompileAndRun()
function:
function! PotionCompileAndRunFile()
silent !clear
execute "!" . g:potion_command . " " . bufname("%")
endfunction
First we run a silent !clear
command, which should clear the screen without
a Press ENTER...
prompt. This will make sure we only see the output of this
run, which is helpful when you're running the same commands over and over.
The next line uses our old friend execute
to build a command dynamically. The
command it builds will look something like this:
!potion factorial.pn
Notice that there's no silent
here, so the user will see the output of the
command and will have to press enter to go back to Vim. This is what we want
for this particular mapping, so we're all set.
The Potion compiler has an option that will let you view the bytecode it generates as it compiles. This can be handy if you're trying to debug your program at a very low level. Try it out by running the following command at a shell prompt:
potion -c -V factorial.pn
You should see a lot of output that looks like this:
-- parsed --
code ...
-- compiled --
; function definition: 0x109d6e9c8 ; 108 bytes
; () 3 registers
.local factorial ; 0
.local print_line ; 1
.local print_factorial ; 2
...
[ 2] move 1 0
[ 3] loadk 0 0 ; string
[ 4] bind 0 1
[ 5] loadpn 2 0 ; nil
[ 6] call 0 2
...
Let's add a mapping that will let the user view the bytecode generated for the current Potion file in a Vim split so they can easily navigate and examine it.
First, add the following line to the bottom of ftplugin/potion/running.vim
:
nnoremap <buffer> <localleader>b :call PotionShowBytecode()<cr>
Nothing special there -- it's just a simple mapping. Now let's sketch out the function that will do the work:
function! PotionShowBytecode()
" Get the bytecode.
" Open a new split and set it up.
" Insert the bytecode.
endfunction
Now that we've got a little skeleton set up, let's talk about how to make it happen.
There are a number of ways we could implement this, so I'll choose one that will come in handy later for you.
Run the following command:
:echom system("ls")
You should see the output of the ls
command at the bottom of your screen. If
you run :messages
you'll see it there too. The system()
Vim function takes
a command string as a parameter and returns the output of that command as
a String.
You can pass a second string as an argument to system()
. Run the following
command:
:echom system("wc -c", "abcdefg")
Vim will display 7
(with some padding). If you pass a second argument like
this, Vim will write it to a temporary file and pipe it into the command on
standard input. For our purposes we won't need this, but it's good to know.
Back to our function. Edit PotionShowBytecode()
to fill out the first part of
the skeleton like this:
function! PotionShowBytecode()
" Get the bytecode.
let bytecode = system(g:potion_command . " -c -V " . bufname("%"))
echom bytecode
" Open a new split and set it up.
" Insert the bytecode.
endfunction
Go ahead and try it out by saving the file, running :set ft=potion
in
factorial.pn
to reload it, and using the <localleader>b
mapping. Vim should
display the bytecode at the bottom of the screen. Once you can see it's working
you can remove the echom
line.
Next we're going to open up a new split window for the user to show the results. This will let the user view and navigate the bytecode with all the power of Vim, instead of just reading it once from the screen.
To do this we're going to create a "scratch" split: a split containing a buffer
that's never going to be saved and will be overwritten each time we run the
mapping. Change the PotionShowBytecode()
function to look like this:
function! PotionShowBytecode()
" Get the bytecode.
let bytecode = system(g:potion_command . " -c -V " . bufname("%"))
" Open a new split and set it up.
vsplit __Potion_Bytecode__
normal! ggdG
setlocal filetype=potionbytecode
setlocal buftype=nofile
" Insert the bytecode.
endfunction
These new command should be pretty easy to follow.
vsplit
creates a new vertical split for a buffer named __Potion_Bytecode__
.
We surround the name with underscores to make it clearer to the user that this
isn't a normal file (it's a buffer just to hold the output). The underscores
aren't special, they're just a convention.
Next we delete everything in this buffer with normal! ggdG
. The first time
the mapping is run this won't do anything, but subsequent times we'll be reusing
the __Potion_Bytecode__
buffer, so this clears it.
Next we prepare the buffer by setting two local settings. First we set its
filetype to potionbytecode
, just to make it clear what it's holding. We also
change the buftype
setting to nofile
, which tells Vim that this buffer isn't
related to a file on disk and so it should never try to write it.
All that's left is to dump the bytecode that we saved into the bytecode
variable into this buffer. Finish off the function by making it look like this:
function! PotionShowBytecode()
" Get the bytecode.
let bytecode = system(g:potion_command . " -c -V " . bufname("%") . " 2>&1")
" Open a new split and set it up.
vsplit __Potion_Bytecode__
normal! ggdG
setlocal filetype=potionbytecode
setlocal buftype=nofile
" Insert the bytecode.
call append(0, split(bytecode, '\v\n'))
endfunction
The append()
Vim function takes two arguments: a line number to append after,
and a list of Strings to append as lines. For example, try running the
following command:
:call append(3, ["foo", "bar"])
This will append two lines, foo
and bar
, below line 3 in your current
buffer. In this case we're appending below line 0, which means "at the top of
the file".
We need a list of Strings to append, but we just have a single string with
newline characters embedded in it from when we used system()
. We use Vim's
split()
function to split that giant hunk of text into a list of Strings.
split()
takes a String to split and a regular expression to find the split
points. It's pretty simple.
Now that the function is complete, go ahead and try out the mapping. When you
run <localleader>b
in the factorial.pn
buffer Vim will open a new buffer
containing the Potion bytecode. Play around with it by changing the source,
saving the file, and running the mapping again to see the bytecode change.
Read :help bufname
.
Read :help buftype
.
Read :help append()
.
Read :help split()
.
Read :help :!
.
Read :help :read
and :help :read!
(we didn't cover these commands, but
they're extremely useful).
Read :help system()
.
Read :help design-not
.
Currently our mappings require that the user save the file themselves before running the mapping in order for their changes to take effect. Undo is cheap these days, so edit the functions we wrote to save the current file for them.
What happens when you run the bytecode mapping on a Potion file with a syntax error? Why does that happen?
Change the PotionShowBytecode()
function to detect when the Potion compiler
returns an error, and show an error message to the user.
Each time you run the bytecode mapping a new vertical split will be created, even if the user hasn't closed the previous one. If the user doesn't bother closing them they could end up with many extra windows stacked up.
Change PotionShowBytecode()
to detect with a window is already open for the
__Potion_Bytecode__
buffer, and when that's the case switch to it instead of
creating a new split.
You'll probably want to read :help bufwinnr()
for this one.
Remember how we set the filetype
of the temporary buffer to potionbytecode
?
Create a syntax/potionbytecode.vim
file and define syntax highlighting for
Potion bytecode buffers to make them easier to read.