Vim setup for C programming

standarizing unconformity

Index


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/

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()

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'

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.

completed Vim(1) setup for 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:

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 (: