1---
2title: Integrated GDB Debugging in Vim with TermDebug
3url: integrated-gdb-debugging-in-vim-with-termdebug.html
4date: 2026-01-09T16:13:13+02:00
5type: post
6draft: false
7tags: []
8---
9
10## Motivation
11
12Over time I have tried making my `~/.vimrc` more universal and capable of
13serving all the different projects, and I always fail. Configuration just
14balloons, and sometimes I need different tools for different projects, that
15fight against each other.
16
17Based on that, I realized that per project `.vimrc` would be much better, and
18would better suit my development style. This way I can keep my main `~/.vimrc`
19short and to the point and have project specific configuration separate.
20
21> **Important**: GDB is amazing but sometimes you do require something with
22> better UI and better ergonomics. If you need a graphical debugger that uses
23> GDB as a backend give https://github.com/nakst/gf a try. If you use gf2 than
24> this post does not apply.
25
26## Main goals and requirements
27
28- Easy to launch `make` and/or run the application I'm working on.
29- If the compilation fails, I want `Quickfix List` to be populated with errors.
30- Start a debugger and break on `main`.
31- Start a debugger and break on `currently highlighted line` in Vim.
32
33## A quick demonstration
34
35<video width="100%" controls>
36 <source src="/assets/posts/vim-gdb/demo.mp4" type="video/mp4">
37</video>
38
39## Main `~/.vimrc`
40
41My `~/.vimrc` is very minimal. I only use these four plugins:
42
43- https://github.com/tpope/vim-commentary - comment stuff out
44- https://github.com/ctrlpvim/ctrlp.vim - fuzzy file, buffer finder
45- https://github.com/dense-analysis/ale - LSP integration
46- https://github.com/mitjafelicijan/sniper.vim - buffer bookmark manager
47
48At the top of my configuration file I have the following.
49
50```vimrc
51set nocompatible exrc secure
52```
53
54- `exrc` - Allows Vim to read local configuration files.
55- `secure` - Restricts what local vimrc/exrc files are allowed to do.
56
57`exrc` is the important one. This allows us to have `.vimrc` in the directory
58of our project. And this will then only apply to that specific project while
59not polluting the main `~/.vimrc` file.
60
61## Project `~/project/foo/.vimrc`
62
63The project for testing showcasing this will be a simple C project using [GNU
64Make](https://www.gnu.org/software/make/) and [Clang](https://clang.llvm.org/).
65
66Project structure
67
68```
69~/Projects/cproject
70 main.c
71 Makefile
72 .vimrc
73```
74
75```Makefile
76# Makefile
77cprogram: main.c
78 clang -g -o cprogram main.c
79```
80
81```c
82// main.c
83
84#include <stdio.h>
85#include <stdlib.h>
86
87typedef struct {
88 int q;
89 int w;
90} Bar;
91
92int main(void) {
93 const char *myenv = getenv("MYENV");
94
95 int a = 100;
96 int b = 123;
97 int c = a + b;
98
99 Bar bar = { .q = 565, .w = 949 };
100
101 printf("> MYENV: %s\n", myenv);
102 printf("> c: %d\n", c);
103 printf("> bar.q: %d\n", bar.q);
104
105 for (int i=0; i<10; i++) {
106 printf("> loop %d\n", i);
107 }
108
109 return 0;
110}
111```
112
113```vim
114" .vimrc
115let g:_executable = 'cprogram'
116let g:_arguments = ''
117let g:_envs = { 'MYENV': 'howdy' }
118let g:_make = 'make -B'
119
120set makeprg=make
121set errorformat=%f:%l:%c:\ %m
122packadd termdebug
123
124let g:termdebug_config = {}
125let g:termdebug_config['variables_window'] = v:true
126
127nnoremap <leader>x :call LocalRun()<CR>
128nnoremap <leader>c :call LocalMake()<CR>
129nnoremap <leader>m :call LocalDebugMain()<CR>
130nnoremap <leader>l :call LocalDebugLine()<CR>
131
132function! LocalRun() abort
133 let envs = join( map(items(g:_envs), { _, kv -> kv[0] . '=' . kv[1] }), ' ')
134 execute printf("term env %s ./%s %s", envs, g:_executable, g:_arguments)
135endfunction
136
137function! LocalDebugMain() abort
138 execute printf('Termdebug %s %s', g:_executable, g:_arguments)
139
140 for [k, v] in items(g:_envs)
141 call TermDebugSendCommand(printf('set env %s %s', k, v))
142 endfor
143
144 call TermDebugSendCommand('break main')
145 call TermDebugSendCommand('run')
146endfunction
147
148function! LocalDebugLine() abort
149 let cmd = printf("break %s:%d", expand('%'), line('.'))
150 execute printf('Termdebug %s %s', g:_executable, g:_arguments)
151
152 for [k, v] in items(g:_envs)
153 call TermDebugSendCommand(printf('set env %s %s', k, v))
154 endfor
155
156 call TermDebugSendCommand(cmd)
157 call TermDebugSendCommand('run')
158endfunction
159
160function! LocalMake() abort
161 for [k, v] in items(g:_envs)
162 execute printf("let $%s = %s", k, string(v))
163 endfor
164
165 silent make
166
167 " Filter non valid errors out of quicklist.
168 let qfl = getqflist()
169 let filtered = filter(copy(qfl), {_, entry -> entry.valid == 1})
170 call setqflist(filtered, 'r')
171
172 redraw!
173 execute len(filtered) > 0 ? 'copen' : 'cclose'
174endfunction
175```
176
177> **Note**: If any errors are found during the compilation mode the Quickfix
178> list will be populated and opened. You could use CtrlP Quickfix with
179> `:CtrlPQuickfix` instead of `:copen` for this as well, I just want to stick
180> to what Vim supports natively.
181
182Lets check these keybindings.
183
184- `Leader+x` - runs the binary in the terminal above and providing environment variables and argument
185- `Leader+c` - runs make and puts error in Quickfix List then opens with `:copen`
186- `Leader+m` - launches DebugTerm/DBG debugger with all variables and breaks on main
187- `Leader+l` - launches DebugTerm/DBG debugger with all variables and breaks on current line
188
189This setup can get even more elaborate. Depending on your needs. This example
190works really well for projects using C, but anything goes here.
191
192## Why use `:term` and `:TermDebug`
193
194It's very easy to yank and paste from internal terminal buffers even if you are
195not using `tmux` as multiplexer. Just makes the whole thing much easier. I do
196however use `tmux` as well but for compile/debug loop this proved to be a much
197better experience.
198
199`:TermDebug` is also a no brainier. The integration of GDB directly in Vim
200makes adding new breakpoints with `:Break` just seamless. This goes for all
201other commands as well. You can read more about other commands with `:h
202Termdebug` or on https://vimhelp.org/terminal.txt.html.
203
204## Isn't this setup a bit elaborate?
205
206Yes and no. I don't change these files much at all. When I start a new C
207project I just copy an existing one from a different project to my new one and
208call it a day. And any significantly complex project will require certain per
209project specific configurations anyway.
210
211This also makes it shareable with other people without dumping your whole setup
212on them. There are drawback like with anything else, but I have found this
213approach to be the lesser of two evils kinda think. Even Vim users can ignore
214these files by not enabling local configuration files.
215
216Like any other tool, it's on you to make your development workflow as
217frictionless as possible and no tool is perfect.