Vim setup for C programming
Index
- What do we need?
- Basic vim configuration
- Autocompletion
- The plugins
- The C sandbox generation script
- Afterword
There are several guides and methods out there to setup the Vim(1) editor to make it behave as an IDE for the C programming language, but today we are going to give a look at the one that I've setup inside FreeBSD, which is the one I currently use to develop inside the platform.
If you are not using FreeBSD don't worry, the setup should work in other *nix systems too.
What do we need?
First of all, Vim(1). It mostly will be installed in your system, but if not, just grab your package manager and install it. There are several flavors of the package, ones with more extra utilities built in, others with a graphical user interface, and some tailored in a minimal command-line program.
Then we need to setup some basic configuration lines inside the .vimrc
file, install a language server for auto-completion and get some plugins to integrate everything inside the editor.
Basic vim configuration
Inside your home directory you should find a file named .vimrc
. If not, then just create it, it's a plain text file where we tell Vim(1) how to behave when launched.
By default, Vim(1) comes with minimum configuration instructions set. Let's start by telling Vim(1) to associate C file extensions to the language:
augroup project
autocmd!
autocmd BufRead,BufNewFile *.h,*.c set filetype=c
augroup END
Next we can specify how it should behave with tabulations and spaces.
set tabstop=4
set shiftwidth=4
set noexpandtab
set list listchars=tab:\⟶\ ,trail:·,extends:>,precedes:<,nbsp:%
set lcs+=space:·
set wildmenu
" Tab indenting
vmap <Tab> >gv
vmap <S-Tab> <gv
One basic thing that most code editors do is to match pairs when a parenthesis bracket or quote is typed, that is, adding the opposite character automatically so we don't have to bother closing it.
" Auto-close pairs
set showmatch
inoremap " ""<left>
inoremap ' ''<left>
inoremap ( ()<left>
inoremap [ []<left>
inoremap { {}<left>
au BufRead,BufNewFile *.c,*.h inoremap /* /**/<left><left>
Note that by using
au BufRead,BufNewFile
and specifying the file extension, we can set rules that apply only when programming in a specific programming language.
Going a bit further, when you select some code in visual mode, code editors like Visual Studio Code allow the user to surround the selection with special characters (parenthesis, quotes, etc.). We can tell Vim(1) to do so too.
xnoremap <nowait> ( c()<ESC>P
xnoremap <nowait> " c""<ESC>P
xnoremap <nowait> ` c``<ESC>P
xnoremap <nowait> [ c[]<ESC>P
Now, the coolest part of every code editor is the autocompletion. There are many workarounds to activate autocompletion inside Vim(1), even just using native instructions from Vim(1), but for this setup we are going to mix internal instructions with external plugins. For now let's just add the following rule to the config file:
set completeopt=menu,menuone,preview,noinsert,noselect
Lastly, as for the basic Vim(1) configuration, let's setup text wrapping and a visual indicator for the line limit. You can tweak the cc
rule to give the lines the character length limit you want.
set wrap
set linebreak
set cc=100
set number
Autocompletion
Let's focus in the code completion now. We've setup a basic instruction line for Vim(1) to behave in this field, but to unleash the power of environment awareness in the project, we can get some external help.
This help comes from a Language Server Protocol (LSP). This protocol enables communication between our editor (acting as a client) and the language services that our language server provides.
We do have and editor, the next thing we need is a LSP that works with it. One of the best bundles out there is LLVM, which comes with a handy tool that adds smart features to our editor.
First thing's first, grab the package into your system (or alternatively build it from source).
$ pkg install llvm15
More information about clangd can be found here
Now let's see how clangd
works. It can be setup in two ways, the first method being by using a compile_commands.json
in the project's root file, and the other one is by setting up a compile_flags.txt
file.
In this case, we are using the second method which seems to work just fine, and it's easier to setup. The compile_flags.txt
file needs some content inside to work, which on its minimum looks as follows:
%c -std=c11
-I/usr/local/include
-Iinc/
The first line argument is parsed only when targeting the C language (
%c
), which in this case sets up the standard.The next line indicates where to parse system headers.
The last line indicates where to parse project headers.
But wait, it ain't working yet. We need the final key to glue all the pieces together.
The plugins
The list of plugins needed is small, we can make it work with 3 plugins (and you can maybe even lower that number!). To install them, you can use your preferred method, the guide uses VimPlug:
call plug#begin()
Plug 'vim-scripts/vimcompletesme'
Plug 'natebosch/vim-lsc'
Plug 'dense-analysis/ale'
call plug#end()
vimcompletesme is a super simple, super minimal, super light-weight tab-completion plugin for Vim(1).
vim-lsc adds language-aware tooling to Vim(1) by communicating with a language server following the language server protocol.
ale (Asynchronous Lint Engine) is a plugin providing linting (syntax checking and semantic errors) in Vim(1) while you edit your text files.
Tuning the plugins
The first plugin, vimcompletesme
doesn't require configuration at all, since we already specified the completion methods for Vim(1) at the beginning. For vim-lsc
we need to tweak some configuration parts. Its final form looks like this:
" vim-lsc ---------------------------------------------
" Use all the defaults (recommended):
" let g:lsc_auto_map = v:true
" Tell lsc to use clangd, note on FreeBSD you need to call it like clangd15
let g:lsc_server_commands = {
\ 'c': {
\ 'command': 'clangd',
\ 'log_level': -1,
\ 'suppress_stderr': v:true,
\ }
\}
" Tweak some defaults to our needs
let g:lsc_auto_map = {
\ 'GoToDefinition': 'gd',
\ 'FindReferences': 'gr',
\ 'Rename': 'gR',
\ 'ShowHover': v:true,
\ 'FindCodeActions': 'ga',
\ 'Completion': 'completefunc',
\}
" Finally enable completion, and disable annoying errors and diagnosis on the screen, we have ale for that
let g:lsc_enable_autocomplete = v:true
let g:lsc_enable_diagnostics = v:false
let g:lsc_reference_highlights = v:false
let g:lsc_trace_level = 'off'
let g:lsc_suppress_stderr = v:true
In the ale
plugin side, as we mentioned early, we are only interested in the linter part. The final configuration looks like this:
" ale vim -------------------------------------------
" Note in FreeBSD you need to set the clangd_executable variable
let g:ale_linters = {'c': ['clangd']}
let g:ale_sign_error = '*'
let g:ale_sign_warning = '!'
let g:ale_echo_msg_format = '[%linter%] %s [%severity%]'
let g:ale_lint_on_enter = 1
let g:ale_c_clangd_executable = 'clangd15'
The option
ale_linters
allows us to add the file extension associated with a language server. As you may have noticed, we can have more than one linter, and more than one file extension configured at the same time.ale_sign_error
andale_sign_warning
are the characters you canale
to print in the left side of Vim(1) when an error or a warning is detected.ale_echo_msg_format
allows us to customize the formatting of the printed messages when there's something to inform.ale_lint_on_enter
can be set so the linter acts each time we open a file. It will act always on save whether this variable is set or not.ale_c_clangd_executable
specifies the clangd binary to execute.
After following the complete setup steps, we should have a working Vim(1) editor that can act similar to a basic IDE for C/C++ programming.
In order to level up the setup a bit, let's implement a shell script that we can call each time we want to setup a new C project, so we don't have to bother about makefiles, clangd config files, directory structures and such.
The C sandbox generation script
The first thing we need to ensure, is that we are working in the correct directory path. We can check it with a quick pwd(1)
command, and ask the user if we are in the right place. If not, we finish the program.
If the working path is correct, then we can ask for the project's name, and use it as the main directory. Inside it we can create the following subdirectories:
/src
for the source files/inc
for the header files/bin
for the compiled binaries/doc
for documentation files/3rd-party
for external libraries used
As an extra step, and in order to avoid asking for more instructions later on the script's execution, we can also ask the user about which compiler should the setup use, and provide a default just in case.
#!/bin/sh
#custom printf function to speed up instruction writing
printfn(){
local msg="${@}"
printf "%s\\n" "${msg}"
}
# read current working dir
WORKDIR=$(pwd)
generate_project_base() {
printfn "current work dir is: " "${WORKDIR}"
read -p "is this correct? [Y/n] " correct_wd
if [ "${correct_wd}" != Y ] && [ "${correct_wd}" != y ]; then
return 1
fi
# read user input for project name
read -p "what is the project name: " proj_name
if [ -z "${proj_name}" ]; then
return 1
fi
# read user preferences for project
# read compiler
read -p "which compiler are you using [default is clang]: " u_compiler
# create directories:
printf "generating directory structure at ${WORKDIR}/${proj_name}... "
# project dir with name
mkdir "${proj_name}"
# src
mkdir "${proj_name}"/src
# inc
mkdir "${proj_name}"/inc
# bin
mkdir "${proj_name}"/bin
# doc
mkdir "${proj_name}"/doc
printfn "DONE"
}
The following step involves creating a simple generic Makefile that can be used as an initial template. We are covering the path locations for the code, some basic CFLAGS
and some initial LDFLAGS
.
We also include some general instructions to build the code, install the generated binary, and clean all the generated objects and executables if required.
Note that echoing some characters for the Makefile require an extra
\
.
generate_makefile() {
printf "generating initial Makefile... "
echo "CC = ${u_compiler:-clang}
SRCS != ls src/*.c
OBJS = \$(SRCS:.c=.o)
DIST = bin
INC = -Iinc -I/usr/local/include
CFLAGS = -c -g -Wall \$(INC) -fsanitize=address
LDFLAGS = -L/usr/local/lib -fsanitize=address
EXEC_NAME = test
## build exec ##
all: output install
output: \$(OBJS)
@echo Building \$(EXEC_NAME) ...
\$(CC) \$(OBJS) \$(LDFLAGS) -o \$(EXEC_NAME)
\$(OBJS): \$(SRCS)
@echo Compiling srcs ...
\$(CC) \$(CFLAGS) -c $< -o \$@
## remove things ##
.PHONY: clean install
install:
@echo Moving \$(EXEC_NAME) into \$(DIST)...
@if [ -f \$(EXEC_NAME) ]; then\\
mv \$(EXEC_NAME) \$(DIST)/\$(EXEC_NAME);\\
fi
clean:
rm -f src/*.o
rm -f \"\$(DIST)/\$(EXEC_NAME)\";
" > "${proj_name}"/Makefile
printfn "DONE"
}
The compile_flags.txt file can also be automated in order to have an initial template to work with. Since we asked the user for a compiler option, we can take advantage in this file too as we did with the makefile, and pass the user value.
generate_cflags(){
# create initial compile_flags.txt
printf "generating initial compile_flags.txt... "
echo "%c -std=c11
-isystem/usr/local/include
-Iinc/
" > "${proj_name}"/compile_flags.txt
printfn "DONE"
}
As many IDEs do, we can populate the main entry file for the C program with the mandatory function, and provide the user with a ready to use main.c
file.
generate_main(){
# create main.c file
printf "generating entry point... "
echo "#include <stdio.h>
int main(int argc, char **argv) {
return 0;
}
" > "${proj_name}"/src/main.c
printfn "DONE"
}
Last thing we need in the script is to call all the functions in order.
generate_project_base
generate_makefile
generate_ccls
generate_main
Afterword
A complete configuration can be found at n0mad's Codeberg repository.
This is a really tailored way to work with Vim(1) in C/C++ programming, that may not fit in everyone's needs. On future guides we'll expand it to work with embedded development too. Until then, happy coding (: