--- title: Integrated GDB Debugging in Vim with TermDebug url: integrated-gdb-debugging-in-vim-with-termdebug.html date: 2026-01-09T16:13:13+02:00 type: post draft: false tags: [] --- ## Motivation Over time I have tried making my `~/.vimrc` more universal and capable of serving all the different projects, and I always fail. Configuration just balloons, and sometimes I need different tools for different projects, that fight against each other. Based on that, I realized that per project `.vimrc` would be much better, and would better suit my development style. This way I can keep my main `~/.vimrc` short and to the point and have project specific configuration separate. > **Important**: GDB is amazing but sometimes you do require something with > better UI and better ergonomics. If you need a graphical debugger that uses > GDB as a backend give https://github.com/nakst/gf a try. If you use gf2 than > this post does not apply. ## Main goals and requirements - Easy to launch `make` and/or run the application I'm working on. - If the compilation fails, I want `Quickfix List` to be populated with errors. - Start a debugger and break on `main`. - Start a debugger and break on `currently highlighted line` in Vim. ## A quick demonstration ## Main `~/.vimrc` My `~/.vimrc` is very minimal. I only use these four plugins: - https://github.com/tpope/vim-commentary - comment stuff out - https://github.com/ctrlpvim/ctrlp.vim - fuzzy file, buffer finder - https://github.com/dense-analysis/ale - LSP integration - https://github.com/mitjafelicijan/sniper.vim - buffer bookmark manager At the top of my configuration file I have the following. ```vimrc set nocompatible exrc secure ``` - `exrc` - Allows Vim to read local configuration files. - `secure` - Restricts what local vimrc/exrc files are allowed to do. `exrc` is the important one. This allows us to have `.vimrc` in the directory of our project. And this will then only apply to that specific project while not polluting the main `~/.vimrc` file. ## Project `~/project/foo/.vimrc` The project for testing showcasing this will be a simple C project using [GNU Make](https://www.gnu.org/software/make/) and [Clang](https://clang.llvm.org/). Project structure ``` ~/Projects/cproject main.c Makefile .vimrc ``` ```Makefile # Makefile cprogram: main.c clang -g -o cprogram main.c ``` ```c // main.c #include #include typedef struct { int q; int w; } Bar; int main(void) { const char *myenv = getenv("MYENV"); int a = 100; int b = 123; int c = a + b; Bar bar = { .q = 565, .w = 949 }; printf("> MYENV: %s\n", myenv); printf("> c: %d\n", c); printf("> bar.q: %d\n", bar.q); for (int i=0; i<10; i++) { printf("> loop %d\n", i); } return 0; } ``` ```vim " .vimrc let g:_executable = 'cprogram' let g:_arguments = '' let g:_envs = { 'MYENV': 'howdy' } let g:_make = 'make -B' set makeprg=make set errorformat=%f:%l:%c:\ %m packadd termdebug let g:termdebug_config = {} let g:termdebug_config['variables_window'] = v:true nnoremap x :call LocalRun() nnoremap c :call LocalMake() nnoremap m :call LocalDebugMain() nnoremap l :call LocalDebugLine() function! LocalRun() abort let envs = join( map(items(g:_envs), { _, kv -> kv[0] . '=' . kv[1] }), ' ') execute printf("term env %s ./%s %s", envs, g:_executable, g:_arguments) endfunction function! LocalDebugMain() abort execute printf('Termdebug %s %s', g:_executable, g:_arguments) for [k, v] in items(g:_envs) call TermDebugSendCommand(printf('set env %s %s', k, v)) endfor call TermDebugSendCommand('directory ' . getcwd()) call TermDebugSendCommand('break main') call TermDebugSendCommand('run') endfunction function! LocalDebugLine() abort let cmd = printf("break %s:%d", expand('%'), line('.')) execute printf('Termdebug %s %s', g:_executable, g:_arguments) for [k, v] in items(g:_envs) call TermDebugSendCommand(printf('set env %s %s', k, v)) endfor call TermDebugSendCommand(cmd) call TermDebugSendCommand('run') endfunction function! LocalMake() abort for [k, v] in items(g:_envs) execute printf("let $%s = %s", k, string(v)) endfor silent make " Filter non valid errors out of quicklist. let qfl = getqflist() let filtered = filter(copy(qfl), {_, entry -> entry.valid == 1}) call setqflist(filtered, 'r') redraw! execute len(filtered) > 0 ? 'copen' : 'cclose' endfunction ``` > **Note**: If any errors are found during the compilation mode the Quickfix > list will be populated and opened. You could CtrlP Quickfix for this as well, > I just want to stick to what Vim supports natively. Lets check these keybindings. - `Leader+x` - runs the binary in the terminal above and providing environment variables and argument - `Leader+c` - runs make and puts error in Quickfix List then opens with `:copen` - `Leader+m` - launches DebugTerm/DBG debugger with all variables and breaks on main - `Leader+l` - launches DebugTerm/DBG debugger with all variables and breaks on current line This setup can get even more elaborate. Depending on your needs. This example works really well for projects using C, but anything goes here. ## Why use `:term` and `:TermDebug` It's very easy to yank and paste from internal terminal buffers even if you are not using `tmux` as multiplexer. Just makes the whole thing much easier. I do however use `tmux` as well but for compile/debug loop this proved to be a much better experience. `:TermDebug` is also a no brainier. The integration of GDB directly in Vim makes adding new breakpoints with `:Break` just seamless. This goes for all other commands as well. You can read more about other commands with `:h Termdebug` or on https://vimhelp.org/terminal.txt.html.