Tweaking Vim To Be A More Suitable C/C++-IDE

Being a true Vim patriot requires me to share with my fellow brethren my repertoire of handmade tools, lest they should be forsaken. Therefore I will present in this very post some script fragments to drastically improve productivity when developing C/C++ inside Vim.

All of these fragments are part of my .vimrc and can thus be viewed on my dotfiles repo on GitHub.

Include guard insertion

Upon creating a .h or .hpp file this fragment will automatically insert include guards, the macro being based on the filename.

" InsertGates: when creating a file called name.h automatically add include
" guard
function! s:InsertGates()
  let gatename = substitute(toupper(expand("%:t")), "\\.", "_", "g")
  execute "normal! i#ifndef " . gatename
  execute "normal! o#define " . gatename . " "
  execute "normal! Go#endif"
  normal! O
endfunction
autocmd BufNewFile *.{h,hpp} call <SID>InsertGates()

Switching between source and header file

Based on filename this fragment tries to swap extensions from .h to .c/.cpp and vice versa. It additionally attempts to the load the file from an existing buffer and otherwise open the file anew. In my case I mapped this command to the F3 key.

" SwitchHeaderSource: switch between file.cpp and file.h
function! s:SwitchHeaderSource()
  let file_and_ext = split(expand("%:t"), '\.')
  if len(file_and_ext) < 2
    echo 'Invalid filename'
    return
  end
  let this_file = file_and_ext[0]
  let ext = file_and_ext[1]
  let file_to_open = ''
  if match(ext, 'cpp') == 0
    let file_to_open = this_file . '.h'
  elseif match(ext, 'c') == 0
    let file_to_open = this_file . '.h'
  elseif match(ext, 'h') == 0
    if !empty(glob(this_file.'.cpp'))
      let file_to_open = this_file . '.cpp'
    elseif !empty(glob(this_file.'.c'))
      let file_to_open = this_file . '.c'
    end
  else
    echo 'Not a C/C++ extension: ' . ext
  endif

  if bufnr(file_to_open) > 0
    exec 'buffer ' . file_to_open
  else
    exec 'e ' . file_to_open
  endif
endfunction

command! -nargs=+ SwitchHeaderSource call s:SwitchHeaderSource()
autocmd BufNew,BufRead *.{c,cpp,h,cxx,hpp} nnoremap <F3> :SwitchHeaderSource()<CR>

Execute a command inside a buffer

This feature is a prerequisite for reliably launching build scripts inside Vim. If triggered this will open a right vertical split and paste in the executed command’s output. Note that because of the synchronous nature of Vim, command execution will block until termination. You might have more luck using NeoVim’s terminal feature — these are asynchronous and even interactive. Since my builds don’t take more than five seconds at most I’m fine with some delay.

" ExecuteInShell: execute a given command and redirect its output in a new buffer
"
" TODO: this waits until the command is finished, causing a delay. this might
" be annoying but for my use case it's acceptable. might wanna try using
" neovim's terminal feature in the future
function! s:ExecuteInShell(command)
  let command = join(map(split(a:command), 'expand(v:val)'))
  let winnr = bufwinnr('^' . command . '$')
  silent! execute  winnr < 0 ? 'botright vnew ' . fnameescape(command) : winnr . 'wincmd w'
  setlocal buftype=nowrite bufhidden=wipe nobuflisted noswapfile nowrap nonu
  echo 'Execute ' . command . '...'
  silent! execute 'silent %!'. command
  silent! execute 'resize '
  silent! redraw
  silent! execute 'au BufUnload <buffer> execute bufwinnr(' . bufnr('#') . ') . ''wincmd w'''
  silent! execute 'nnoremap <silent> <buffer> <LocalLeader>r :call <SID>ExecuteInShell(''' . command . ''')<CR>'
  echo 'Shell command ' . command . ' executed.'
endfunction
command! -complete=shellcmd -nargs=+ Shell call s:ExecuteInShell(<q-args>)

Build inside Vim

This will find a build.bat or Makefile (nothing special, just the two build methods I employ the most frequently) inside the current directory and launch a build using the established ExecuteInShell command. You probably want to adjust this feature to your specific build process. Because I was previously using Visual Studio this gets mapped to F5.

" ExecuteBuildCommand: execute a build.bat or Makefile in the current
" directory using ExecuteInShell
function! s:ExecuteBuildCommand()
  silent! execute 'w'
  let shellargs = ''
  if has("win32")
    if !empty(glob(expand("%:p:h\\build.bat")))
      let shellargs = shellargs . ' && build'
    endif
  else
    if !empty(glob(expand("%:p:h/Makefile")))
      let shellargs = shellargs . ' && make'
    endif
  endif
  silent! execute 'cd' . expand("%p:h")
  silent! execute 'Shell cd %:p:h'.shellargs
  silent! execute 'set filetype=msvc'
  silent! execute 'wincmd p'
endfunction
command! -complete=shellcmd -nargs=+ Build call s:ExecuteBuildCommand()

" Map F5 to the build command
autocmd BufNew,BufRead *.{c,cpp,h,cxx,hpp} noremap <F5> :Build()<CR>

Jump directly to compiler messages

Now comes the icing of the cake. Say you launched a build and there’s warnings and errors. Placing the cursor on the same line as the message and pressing Enter will try and find this particular location and open the corresponding file. I have come so far as to make this compatible for both Microsoft’s and GCC’s compiler output.

" GoToError: directly jump to the line of code of the error hovered on by the
" cursor. used in conjunction with ExecuteBuildCommand
"
" example:
"   c:\dev\ars\ars.cpp(62): error C2065: 'xxx': undeclared identifier
"
" NOTE: works for MSVC and GCC
function! s:GoToError()
  let line = split(getline("."))
  if len(line) < 1
    return
  end

  if has("win32")
    let file_and_line = split(line[0], "(")
    if len(file_and_line) < 2
      let file_and_line = split(line[0], "|")
    endif
    if len(file_and_line) < 2
      echo "Cannot parse!"
      return
    endif
    let the_file = file_and_line[0]
    let line_number = split(file_and_line[1], ")")[0]
  else
    let file_and_line = split(line[0], ":")
    if len(file_and_line) < 2
      echo "Cannot parse!"
      return
    endif

    let the_file = file_and_line[0]
    let line_number = file_and_line[1]
  end
  let winnr = bufwinnr(the_file)
  if winnr > 0
    exec winnr . 'wincmd w'
  else
    exec 'wincmd p'
    exec "e " .  the_file
  endif
  exec  'normal! ' . line_number . 'G'
endfunction

command! -nargs=+ GoToError call s:GoToError()
autocmd BufNew,BufRead *.{c,cpp,h,cxx,hpp} nnoremap <Return> :GoToError()<CR>