Skip to content

Instantly share code, notes, and snippets.

@Konfekt
Last active March 6, 2026 14:59
Show Gist options
  • Select an option

  • Save Konfekt/2d951b9e07831878b1476133c5f37b52 to your computer and use it in GitHub Desktop.

Select an option

Save Konfekt/2d951b9e07831878b1476133c5f37b52 to your computer and use it in GitHub Desktop.
Automatically generate compile_commands.json for clangd Language Server in Vim; put into ~/.vim/ftplugin/
" Generate compile_commands.json for clangd.
" Prefer CMake export, else Ninja compdb, else Bear + Make with optional append.
if exists('b:did_c_compdb_ftplugin') | finish | endif
let b:did_c_compdb_ftplugin = 1
let s:inflight = {}
augroup c_compdb
autocmd! * <buffer>
autocmd BufWinEnter <buffer> ++once call s:maybe_compdb()
augroup END
" -- User command to force regeneration --------------------------
command! -buffer CompileDbGenerate call s:maybe_compdb(v:true)
function! s:maybe_compdb(...) abort
let force = get(a:, 1, v:false)
let root = s:project_root()
if empty(root) | return | endif
if has_key(s:inflight, root) | return | endif
if !force && filereadable(root . '/compile_commands.json') | return | endif
if !force && filereadable(s:build_dir(root) . '/compile_commands.json')
call s:sync_from_build(root)
return
endif
let s:inflight[root] = 1
if filereadable(root . '/CMakeLists.txt')
call s:cmake_export_compdb(root)
return
endif
if filereadable(s:build_dir(root) . '/build.ninja') || filereadable(root . '/build.ninja')
call s:ninja_compdb(root)
return
endif
if !empty(glob(root . '/[Mm]akefile', 1, 1))
call s:bear_make(root)
return
endif
call s:done(root)
endfunction
function! s:project_root() abort
let start = expand('%:p:h')
for m in ['CMakeLists.txt', 'Makefile', 'makefile', 'build.ninja', '.git']
if m ==# '.git'
let d = finddir(m, start . ';')
if !empty(d) | return fnamemodify(d, ':p:h:h') | endif
else
let f = findfile(m, start . ';')
if !empty(f) | return fnamemodify(f, ':p:h') | endif
endif
endfor
" Don't guess --- avoid scribbling in $HOME or an unrelated cwd.
return ''
endfunction
function! s:build_dir(root) abort
return a:root . '/' . get(g:, 'c_compdb_build_dir', 'build')
endfunction
function! s:done(root) abort
silent! call remove(s:inflight, a:root)
endfunction
" -- Safe deferred echo (exit_cb context is not UI-safe) ---------
function! s:echo(msg) abort
call timer_start(0, {-> execute('echomsg ' .. string(a:msg), '')})
endfunction
" -- CMake -------------------------------------------------------
function! s:cmake_export_compdb(root) abort
if !executable('cmake')
call s:echo('c_compdb: cmake not found')
call s:done(a:root)
return
endif
let bdir = s:build_dir(a:root)
call mkdir(bdir, 'p')
let args = ['cmake', '-S', a:root, '-B', bdir, '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON']
if get(g:, 'c_compdb_prefer_ninja', 1) && executable('ninja') && !filereadable(bdir . '/CMakeCache.txt')
call extend(args, ['-G', 'Ninja'])
endif
call s:echo('c_compdb: configuring CMake ...')
call s:job_start(args, a:root, function('s:on_cmake_configure', [a:root]))
endfunction
function! s:on_cmake_configure(root, job, status) abort
if a:status != 0
call s:echo('c_compdb: CMake configure failed (exit ' .. a:status .. ')')
call s:done(a:root)
return
endif
if s:sync_from_build(a:root)
call s:echo('c_compdb: compile_commands.json synced from build dir')
else
call s:echo('c_compdb: compile_commands.json not found after configure')
endif
call s:done(a:root)
endfunction
" -- Sync helper -------------------------------------------------
function! s:sync_from_build(root) abort
let src = s:build_dir(a:root) . '/compile_commands.json'
let dst = a:root . '/compile_commands.json'
if !filereadable(src) | return 0 | endif
" Symlinks are instant but may need privileges on Windows.
if !has('win32') && executable('cmake')
silent call system('cmake -E create_symlink ' .. shellescape(src) .. ' ' .. shellescape(dst))
if v:shell_error == 0 && filereadable(dst)
return 1
endif
endif
" Portable fallback: copy the file.
try
call writefile(readfile(src, 'b'), dst, 'b')
catch
return 0
endtry
return filereadable(dst)
endfunction
" -- Ninja -------------------------------------------------------
function! s:ninja_compdb(root) abort
if !executable('ninja')
call s:echo('c_compdb: ninja not found')
call s:done(a:root)
return
endif
let bdir = filereadable(s:build_dir(a:root) . '/build.ninja') ? s:build_dir(a:root) : a:root
let out = a:root . '/compile_commands.json'
" Ninja versions older than 1.10 require explicit rule names.
" `cc` and `cxx` cover the common built-in Ninja rules.
let rules = get(g:, 'c_compdb_ninja_rules', ['cc', 'cxx'])
let args = ['ninja', '-C', bdir, '-t', 'compdb'] + rules
call s:echo('c_compdb: running ninja -t compdb ...')
call s:job_start(args, a:root, function('s:on_ninja_compdb', [a:root, out]), {'out': out})
endfunction
function! s:on_ninja_compdb(root, out, job, status) abort
if a:status != 0
call s:echo('c_compdb: ninja -t compdb failed (exit ' .. a:status .. ')')
" Clean up empty / broken output.
if filereadable(a:out) && getfsize(a:out) <= 2
call delete(a:out)
endif
else
call s:echo('c_compdb: compile_commands.json generated via Ninja')
endif
" Clean up stderr log if empty.
let errfile = a:out .. '.err'
if filereadable(errfile) && getfsize(errfile) == 0
call delete(errfile)
endif
call s:done(a:root)
endfunction
" -- Bear + Make -------------------------------------------------
function! s:bear_make(root) abort
if !executable('bear')
call s:echo('c_compdb: bear not found')
call s:done(a:root)
return
endif
if !get(g:, 'c_compdb_auto_make', 1)
call s:echo('c_compdb: make build disabled via g:c_compdb_auto_make')
call s:done(a:root)
return
endif
let out = a:root . '/compile_commands.json'
let append = filereadable(out)
let bear = ['bear', '--output', out]
if append | call add(bear, '--append') | endif
let make = ['make']
if !append && get(g:, 'c_compdb_make_full_on_first', 0)
call add(make, '-B')
endif
call s:echo(append
\ ? 'c_compdb: bear --append + make ...'
\ : 'c_compdb: bear + make ...')
call s:job_start(bear + ['--'] + make, a:root, function('s:on_bear_make', [a:root]))
endfunction
function! s:on_bear_make(root, job, status) abort
if a:status != 0
call s:echo('c_compdb: bear/make failed (exit ' .. a:status .. ')')
else
call s:echo('c_compdb: compile_commands.json updated via Bear')
endif
call s:done(a:root)
endfunction
" -- Portable job wrapper (Vim / Neovim) -------------------------
function! s:job_start(cmd, cwd, exit_cb, ...) abort
let opts = get(a:, 1, {})
if has('nvim')
let nv = {'cwd': a:cwd, 'on_exit': a:exit_cb}
if has_key(opts, 'out')
" Collect stdout into a file manually for Neovim.
let nv._outfile = opts.out
let nv._lines = []
let nv.on_stdout = function('s:nvim_on_stdout')
let nv.on_exit = function('s:nvim_on_exit', [a:exit_cb, opts.out])
endif
call jobstart(a:cmd, nv)
else
let vimopt = {'cwd': a:cwd, 'exit_cb': a:exit_cb}
if has_key(opts, 'out')
let vimopt.out_io = 'file'
let vimopt.out_name = opts.out
let vimopt.err_io = 'file'
let vimopt.err_name = opts.out .. '.err'
endif
call job_start(a:cmd, vimopt)
endif
endfunction
if has('nvim')
function! s:nvim_on_stdout(_id, data, _event) dict abort
call extend(self._lines, a:data)
endfunction
function! s:nvim_on_exit(real_cb, outfile, id, status, _event) abort
if !empty(a:outfile)
call writefile(self._lines, a:outfile, 'b')
endif
call a:real_cb(a:id, a:status)
endfunction
endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment