Configuring NeoVim for C++ Development on OSX in 2021

Lucas Thinnes
Geek Culture
Published in
6 min readAug 18, 2021

--

In attempt to learn C++ in my preferred text editor, I came across a whole bunch of errors even while writing simple programs.

In the 6 hours I spent attempting to debug and silence these errors, I came across a bunch of suggestions and techniques online which did not help much on their own. The reasons for this span across newer OSX machines not saving dependencies in a directory that used to be universal when the original advice was posted, a lack of correct plugins, and certain paths not being recognized or used properly by ZSH or the terminal. In this post, I want to illuminate everything that I did to configure NeoVim properly for C++ development and hope that it can help you out if you are being stressed by a high volume of errors or missing files & libraries!

INSTALLING THE RIGHT DEPENDENCIES

Here is the very first place I started in attempting to configure this right. Upon many searches I discovered that LLVM is the place to start (before brew, node and yarn which you hopefully already have). LLVM is collection of modular compilers which will assist in running C++ smoothly. Next, I installed and built CCLS which is a server for C++. Next, CMake is highly recommended (there is even a section for editor syntax files including Vim). According to the website: “CMake is an extensible, open-source system that manages the build process in an operating system and in a compiler-independent manner”. What I found is that this can assist in compiling and building within the terminal as opposed to in a native compiler. Next, I installed Clang, Clangd and GCC, which also help with clarifying syntax and compiling.

COC.NVIM AND CCLS

Next, using coc.nvim (Conquer of Completion) installed via Vim-Plug, I added all of this code to my init.vim:

" if hidden is not set, TextEdit might fail.
set hidden
" Some servers have issues with backup files, see #649
set nobackup
set nowritebackup
" Better display for messages
set cmdheight=2
" You will have bad experience for diagnostic messages when it's default 4000.
set updatetime=300
" don't give |ins-completion-menu| messages.
set shortmess+=c
" always show signcolumns
set signcolumn=yes
" Use tab for trigger completion with characters ahead and navigate.
" Use command ':verbose imap <tab>' to make sure tab is not mapped by other plugin.
inoremap <silent><expr> <TAB>
\ pumvisible() ? "\<C-n>" :
\ <SID>check_back_space() ? "\<TAB>" :
\ coc#refresh()
inoremap <expr><S-TAB> pumvisible() ? "\<C-p>" : "\<C-h>"
function! s:check_back_space() abort
let col = col('.') - 1
return !col || getline('.')[col - 1] =~# '\s'
endfunction
" Use <c-space> to trigger completion.
inoremap <silent><expr> <c-space> coc#refresh()
" Use <cr> to confirm completion, `<C-g>u` means break undo chain at current position.
" Coc only does snippet and additional edit on confirm.
inoremap <expr> <cr> pumvisible() ? "\<C-y>" : "\<C-g>u\<CR>"
" Use `[c` and `]c` to navigate diagnostics
nmap <silent> [c <Plug>(coc-diagnostic-prev)
nmap <silent> ]c <Plug>(coc-diagnostic-next)
" Remap keys for gotos
nmap <silent> gd <Plug>(coc-definition)
nmap <silent> gy <Plug>(coc-type-definition)
nmap <silent> gi <Plug>(coc-implementation)
nmap <silent> gr <Plug>(coc-references)
" Use K to show documentation in preview window
nnoremap <silent> K :call <SID>show_documentation()<CR>
function! s:show_documentation()
if (index(['vim','help'], &filetype) >= 0)
execute 'h '.expand('<cword>')
else
call CocAction('doHover')
endif
endfunction
" Highlight symbol under cursor on CursorHold
autocmd CursorHold * silent call CocActionAsync('highlight')
" Remap for rename current word
nmap <leader>rn <Plug>(coc-rename)
" Remap for format selected region
xmap <leader>f <Plug>(coc-format-selected)
nmap <leader>f <Plug>(coc-format-selected)
augroup mygroup
autocmd!
" Setup formatexpr specified filetype(s).
autocmd FileType typescript,json setl formatexpr=CocAction('formatSelected')
" Update signature help on jump placeholder
autocmd User CocJumpPlaceholder call CocActionAsync('showSignatureHelp')
augroup end
" Remap for do codeAction of selected region, ex: `<leader>aap` for current paragraph
xmap <leader>a <Plug>(coc-codeaction-selected)
nmap <leader>a <Plug>(coc-codeaction-selected)
" Remap for do codeAction of current line
nmap <leader>ac <Plug>(coc-codeaction)
" Fix autofix problem of current line
nmap <leader>qf <Plug>(coc-fix-current)
" Use <tab> for select selections ranges, needs server support, like: coc-tsserver, coc-python
nmap <silent> <TAB> <Plug>(coc-range-select)
xmap <silent> <TAB> <Plug>(coc-range-select)
xmap <silent> <S-TAB> <Plug>(coc-range-select-backword)
" Use `:Format` to format current buffer
command! -nargs=0 Format :call CocAction('format')
" Use `:Fold` to fold current buffer
command! -nargs=? Fold :call CocAction('fold', <f-args>)
" use `:OR` for organize import of current buffer
command! -nargs=0 OR :call CocAction('runCommand', 'editor.action.organizeImport')
" Add status line support, for integration with other plugin, checkout `:h coc-status`
set statusline^=%{coc#status()}%{get(b:,'coc_current_function','')}
" Using CocList
" Show all diagnostics
nnoremap <silent> <space>a :<C-u>CocList diagnostics<cr>
" Manage extensions
nnoremap <silent> <space>e :<C-u>CocList extensions<cr>
" Show commands
nnoremap <silent> <space>c :<C-u>CocList commands<cr>
" Find symbol of current document
nnoremap <silent> <space>o :<C-u>CocList outline<cr>
" Search workspace symbols
nnoremap <silent> <space>s :<C-u>CocList -I symbols<cr>
" Do default action for next item.
nnoremap <silent> <space>j :<C-u>CocNext<CR>
" Do default action for previous item.
nnoremap <silent> <space>k :<C-u>CocPrev<CR>
" Resume latest coc list
nnoremap <silent> <space>p :<C-u>CocListResume<CR>

Most of this should be annotated as to clarify what is happening. I found this block of code on this site. The rest of the CoC configuration can be launched via :CocConfig while in NeoVim.

Speaking of which, there is a great chunk of code which needs to be added to the :CocConfig:

{
"languageserver": {
"ccls": {
"command": "ccls",
"filetypes": [
"c",
"cpp",
"objc",
"objcpp"
],
"rootPatterns": [
".ccls",
"compile_commands.json",
".vim/",
".git/",
".hg/"
],
"initializationOptions": {
"cache": {
"directory": "/tmp/ccls"
}
}
}
}
}

This is standard configuration for the CCLS server, which supports many different languages beyond simply C++.

FIX BROKEN HEADERS

Ultimately, this is what I needed more than anything else and I have to thank Ian Ding for this solution. The reason why the C++ headers can’t be easily located on newer OSX machines is because the CCLS server will be looking for them in the “/usr/include” directory which no longer exists. The headers are somewhere in limbo as the SDK paths are not hard-coded into this directory. To include the headers for C++ development, run this command in your terminal:

g++ -E -x c++ - -v < /dev/null

This will return a list of places that your compiler will be pulling from. They will appear between the parameters “search starts here” and “end of search list”. What you need to do is copy these paths (or open a new terminal window in tmux to copy over) and place them in a new file in the root directory named .ccls. Above each of these paths, the text -isystem should appear. An example of this configuration will look as such:

-isystem
/usr/local/include
-isystem
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1
-isystem
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/10.0.1/include
-isystem
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include
-isystem
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include

After this, your .cpp project should be able to find the headers and the errors should disappear. I am praying that this solution works for you if you have been struggling with getting C++ properly configured in NeoVim! It took me nearly an entire weekend of experimenting and I am elated to be out of that debacle. While I realize there may be several steps which appear to be overkill (particularly in the installation of many redundant packages), I have found throwing everything at the wall and seeing what sticks to be the best way to get these things working. Please comment below if you have any questions. 🙏

--

--