---
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.