Last active
March 6, 2026 14:59
-
-
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/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| " 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