Integrated GDB Debugging in Vim with TermDebug
post, Jan 9, 2026, on Mitja Felicijan's blog
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:
At the top of my configuration file I have the following.
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 and Clang.
Project structure
~/Projects/cproject
main.c
Makefile
.vimrc
# Makefile
cprogram: main.c
clang -g -o cprogram main.c
// main.c
#include <stdio.h>
#include <stdlib.h>
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;
}
" .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 <leader>x :call LocalRun()<CR>
nnoremap <leader>c :call LocalMake()<CR>
nnoremap <leader>v :call LocalDebugMain()<CR>
nnoremap <leader>b :call LocalDebugLine()<CR>
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
let envs = join( map(items(g:_envs), { _, kv -> kv[0] . '=' . kv[1] }), ' ')
execute printf('silent !env %s %s', g:_make, envs)
" Filter non valid errors out of quicklist.
let qfl = getqflist()
let filtered = filter(copy(qfl), {_, entry -> entry.valid == 1})
call setqflist(filtered, 'r')
redraw!
if len(filtered) > 0
execute exists(':CtrlPQuickfix') ? 'CtrlPQuickfix' : 'copen'
else
cclose
endif
endfunction
I am using the CtrlP plugin, so I also use it for displaying the Quickfix List.
But if the plugin is not found; it will default to native quicklist with
:copen.
Lets check these keybindings.
Leader+x - runs the binary in the terminal above and providing environment variables and argumentLeader+c - runs make and puts error in Quickfix List then opens with :copenLeader+v - launches DebugTerm/DBG debugger with all variables and breaks on mainLeader+b - 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.