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>