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>