Using a simple vimscript, we can easily compile and run any C code without leaving Vim! We will use vim9script for this.
CompileAndRun Function
Functions in vim9script are defined with the def
keyword. Since Vim uses an algol-like syntax, enddef
will denote the end of the functions.
We will define several variables with the var
keyword since vim9script doesn't use the let
keyword.
vim9script
def CompileAndRun()
var current_file = expand('%')
var file_name = fnamemodify(current_file, ':t:r')
As with every vim9script, we start by telling vim that this is in fact vim9script!
Then we get the name of the current file using expand()
, then using the fnamemodifty()
and :t:t
we will strip away the path and the extension, keeping only the basename.
- :t: Takes only the "tail" (or basename) of the file path, removing the directories.
- :r: Removes the extension from the filename.
Typically expand('%')
returns filename.extension if you directly open a file with vim filename.extension
but if the file was opened from a relative path, such as vim ~/some/path/filename.extension
it would also return the path, so we have to clean it up.
We need another variable to store our compile command, for starters, a simple gcc compile command should do:
var compile_cmd = 'gcc ' .. current_file .. ' -o ' .. file_name
The ..
is how we concatenate strings, in vim9script (it used to be one dot in legacy vimscript similar to PHP). You may have also noticed that we are using the extracted file name for our output binary.
After that we can simply compile and execute the output binary.
var compile_result = systemlist(compile_cmd)
execute 'terminal ./' .. file_name
So together:
def CompileAndRun()
var current_file = expand('%')
var file_name = fnamemodify(current_file, ':t:r')
var compile_cmd = 'gcc ' .. current_file .. ' -o ' .. file_name
var compile_result = systemlist(compile_cmd)
execute 'terminal ./' .. file_name
enddef
defcompile
The defcompile
command in the end does exactly what it says, which is compiling our functions into bytecode.
Since vim9script functions are scoped differently than legacy vimscript, we need to define a command to access them.
command! CompileAndRun call CompileAndRun()
Now we can run :CompileAndRun
.
Ok that's fine but what about errors? Warnings? WE NEED THOSE! So let's improve our script!
Let us add a condition that checks for v:shell_error
, see :h v:shell_error
.
if v:shell_error != 0 || !empty(compile_result)
botright new +setlocal\ buftype=nofile\ noswapfile\ bufhidden=wipe
call setline(1, compile_result)
return
endif
Here is a summary, botright new
creates a horizontal split, the +setlocal
options ensure that it's a scratch buffer, systemlist()
executes the binary while handling newlines properly, setline()
puts the results at the first line of the split buffer.
Let's also add -Wall
to our compile command and create an unused variable in our simple test C code.
var compile_cmd = 'gcc -Wall ' .. current_file .. ' -o ' .. file_name
Not bad ey? We can still make it better!
Another perk of using systemlist()
is that it already captured 2>&1 aka stdout and stderr for us so we don't need to perform a redirection.
Can we add syntax highlighting to the warnings and errors? Yes! Just add
set filetype=c
beforecall setline()
.Can we auto-close this split? Yes! But it needs a bit more code.
We will give a name to our split buffer, then using an augroup
auto close it on leave.
if v:shell_error != 0 || !empty(compile_result)
botright new +setlocal\ buftype=nofile\ noswapfile\ bufhidden=wipe
file CompileErrors
set ft=c
call setline(1, compile_result)
return
endif
augroup AutoCloseCompileErrors
autocmd!
autocmd! BufEnter * if bufexists('CompileErrors') && bufname('CompileErrors') != '' | silent! execute 'bdelete! CompileErrors' | endif
augroup END
This ensures that when we change focus from a buffer with the name CompileErrors, the buffer with be deleted and therefor the split will be closed.
However, there may be times when we'd want to keep the split open as we work, so we can create a boolean to set it to 1 or 0 at runtime.
g:close_err_split = true
And we'll modify the augroup
and format it nicely:
augroup AutoCloseCompileErrors
autocmd!
autocmd BufEnter * if g:close_err_split &&
\ bufexists('CompileErrors') &&
\ bufname('CompileErrors') != ''
\ | silent! bdelete! CompileErrors
\ | endif
augroup END
We will be applying the same checks and niceties to the run command as well. So instead of simply running the binary, we can do the following:
g:close_run_split = true
if v:shell_error == 0
botright new +setlocal\ buftype=nofile\ noswapfile\ bufhidden=wipe
resize -10
file RunBin
var results = systemlist('./' .. file_name)
call setline(1, results )
endif
augroup AutoCloseRunBin
autocmd!
autocmd BufEnter * if g:close_run_split &&
\ bufexists('RunBin') &&
\ bufname('RunBin') != ''
\ | silent! bdelete! RunBin
\ | endif
augroup END
You may have noticed that this time we are also determining the size of the split.
Let us add the last touch, which is the ability to define the compile flags at runtime. For that we will have to add a condition to check for a g:custom_compile_flag
and initiate it to be empty at first since vim9script is picky with empty variables.
if !exists('g:custom_compile_flag')
g:custom_compile_flag = ''
endif
And of course, we have to also modify our compile_cmd
:
var compile_cmd = 'gcc ' .. g:custom_compile_flag .. ' ' .. current_file .. ' -o ' .. file_name
Now we can change our compile flags at run time, just don't forget to add a space in the end!
Let's test:
As you can see, we can change our compile flags at runtime without any issues!
The whole script is as follows:
vim9script
def CompileAndRun()
var current_file = expand('%')
var file_name = fnamemodify(current_file, ':t:r')
if !exists('g:custom_compile_flag')
g:custom_compile_flag = ''
endif
var compile_cmd = 'gcc ' .. g:custom_compile_flag .. ' ' .. current_file .. ' -o ' .. file_name
var compile_result = systemlist(compile_cmd)
if v:shell_error != 0 || !empty(compile_result)
botright new +setlocal\ buftype=nofile\ noswapfile\ bufhidden=wipe
file CompileErrors
set ft=c
call setline(1, compile_result)
return
endif
if v:shell_error == 0
botright new +setlocal\ buftype=nofile\ noswapfile\ bufhidden=wipe
resize -10
file RunBin
var results = systemlist('./' .. file_name)
call setline(1, results )
endif
enddef
g:close_err_split = true
g:close_run_split = true
augroup AutoCloseCompileErrors
autocmd!
autocmd BufEnter * if g:close_err_split &&
\ bufexists('CompileErrors') &&
\ bufname('CompileErrors') != ''
\ | silent! bdelete! CompileErrors
\ | endif
augroup END
augroup AutoCloseRunBin
autocmd!
autocmd BufEnter * if g:close_run_split &&
\ bufexists('RunBin') &&
\ bufname('RunBin') != ''
\ | silent! bdelete! RunBin
\ | endif
augroup END
command! CompileAndRun call CompileAndRun()
nnoremap <F8> :CompileAndRun<CR>
defcompile
We've also added the F8 keybinding to access our command more easily.
Note
Vim already has the compiler
and make
commands that you should add to your arsenal. This article only tries to teach some vim9script and some of the things that you can very easily achieve with it.
I hope that you enjoyed this article and if you did, leave a comment or a reaction.