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

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

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.

Other posts