New post: Integrated debugging in Vim

Author Mitja Felicijan <mitja.felicijan@gmail.com> 2026-01-09 11:29:13 +0100
Committer Mitja Felicijan <mitja.felicijan@gmail.com> 2026-01-09 11:29:13 +0100
Commit 3d4a24ef30897fdeab06e3fa7da1086e52db641e (patch)
-rw-r--r-- content/posts/2026-01-09-vim.md 204
-rw-r--r-- static/assets/posts/vim-gdb/demo.mp4 bin 0 B -> 1.4 MB
-rw-r--r-- templates/base.html 10
3 files changed, 206 insertions, 8 deletions
diff --git a/content/posts/2026-01-09-vim.md b/content/posts/2026-01-09-vim.md
  
1
---
  
2
title: Integrated GDB Debugging in Vim with TermDebug
  
3
url: integrated-gdb-debugging-in-vim-with-termdebug.html
  
4
date: 2026-01-09T16:13:13+02:00
  
5
type: post
  
6
draft: false
  
7
tags: []
  
8
---
  
9
  
  
10
## Motivation
  
11
  
  
12
Over time I have tried making my `~/.vimrc` more universal and capable of
  
13
serving all the different projects, and I always fail. Configuration just
  
14
balloons, and sometimes I need different tools for different projects, that
  
15
fight against each other.
  
16
  
  
17
Based on that, I realized that per project `.vimrc` would be much better, and
  
18
would better suit my development style. This way I can keep my main `~/.vimrc`
  
19
short 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
  
  
41
My `~/.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
  
  
48
At the top of my configuration file I have the following.
  
49
  
  
50
```vimrc
  
51
set 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
  
58
of our project. And this will then only apply to that specific project while
  
59
not polluting the main `~/.vimrc` file.
  
60
  
  
61
## Project `~/project/foo/.vimrc`
  
62
  
  
63
The project for testing showcasing this will be a simple C project using [GNU
  
64
Make](https://www.gnu.org/software/make/) and [Clang](https://clang.llvm.org/).
  
65
  
  
66
Project structure
  
67
  
  
68
```
  
69
~/Projects/cproject
  
70
    main.c
  
71
    Makefile
  
72
    .vimrc
  
73
```
  
74
  
  
75
```Makefile
  
76
# Makefile
  
77
cprogram: 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
  
  
87
typedef struct {
  
88
	int q;
  
89
	int w;
  
90
} Bar;
  
91
  
  
92
int 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
  
115
let g:_executable = 'cprogram'
  
116
let g:_arguments = ''
  
117
let g:_envs = { 'MYENV': 'howdy' }
  
118
let g:_make = 'make -B'
  
119
  
  
120
set makeprg=make
  
121
set errorformat=%f:%l:%c:\ %m
  
122
packadd termdebug
  
123
  
  
124
let g:termdebug_config = {}
  
125
let g:termdebug_config['variables_window'] = v:true
  
126
  
  
127
nnoremap <leader>x :call LocalRun()<CR>
  
128
nnoremap <leader>c :call LocalMake()<CR>
  
129
nnoremap <leader>v :call LocalDebugMain()<CR>
  
130
nnoremap <leader>b :call LocalDebugLine()<CR>
  
131
  
  
132
function! 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)
  
135
endfunction
  
136
  
  
137
function! 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('directory ' . getcwd())
  
145
	call TermDebugSendCommand('break main')
  
146
	call TermDebugSendCommand('run')
  
147
endfunction
  
148
  
  
149
function! LocalDebugLine() abort
  
150
        let cmd = printf("break %s:%d", expand('%'), line('.'))
  
151
        execute printf('Termdebug %s %s', g:_executable, g:_arguments)
  
152
  
  
153
        for [k, v] in items(g:_envs)
  
154
                call TermDebugSendCommand(printf('set env %s %s', k, v))
  
155
        endfor
  
156
  
  
157
        call TermDebugSendCommand(cmd)
  
158
        call TermDebugSendCommand('run')
  
159
endfunction
  
160
  
  
161
function! LocalMake() abort
  
162
	let envs = join( map(items(g:_envs), { _, kv -> kv[0] . '=' . kv[1] }), ' ')
  
163
	execute printf('silent !env %s %s', g:_make, envs)
  
164
  
  
165
	" Filter non valid errors out of quicklist.
  
166
	let qfl = getqflist()
  
167
	let filtered = filter(copy(qfl), {_, entry -> entry.valid == 1})
  
168
	call setqflist(filtered, 'r')
  
169
  
  
170
	redraw!
  
171
  
  
172
	if len(filtered) > 0
  
173
		execute exists(':CtrlPQuickfix') ? 'CtrlPQuickfix' : 'copen'
  
174
	else
  
175
		cclose
  
176
	endif
  
177
endfunction
  
178
```
  
179
  
  
180
I am using the CtrlP plugin, so I also use it for displaying the Quickfix List.
  
181
But if the plugin is not found; it will default to native quicklist with
  
182
`:copen`.
  
183
  
  
184
Lets check these keybindings.
  
185
  
  
186
- `Leader+x` - runs the binary in the terminal above and providing environment variables and argument
  
187
- `Leader+c` - runs make and puts error in Quickfix List then opens with `:copen`
  
188
- `Leader+v` - launches DebugTerm/DBG debugger with all variables and breaks on main
  
189
- `Leader+b` - launches DebugTerm/DBG debugger with all variables and breaks on current line
  
190
  
  
191
This setup can get even more elaborate. Depending on your needs. This example
  
192
works really well for projects using C, but anything goes here.
  
193
  
  
194
## Why use `:term` and `:TermDebug`
  
195
  
  
196
It's very easy to yank and paste from internal terminal buffers even if you are
  
197
not using `tmux` as multiplexer. Just makes the whole thing much easier. I don
  
198
however use `tmux` as well but for compile/debug loop this proved to be a much
  
199
better experience.
  
200
  
  
201
`:TermDebug` is also a no brainier. The integration of GDB directly in Vim
  
202
makes adding new breakpoints with `:Break` just seamless. This goes for all
  
203
other commands as well. You can read more about other commands with `:h
  
204
Termdebug` or on https://vimhelp.org/terminal.txt.html.
diff --git a/static/assets/posts/vim-gdb/demo.mp4 b/static/assets/posts/vim-gdb/demo.mp4
diff --git a/templates/base.html b/templates/base.html
...
32
			header nav span.title { font-weight: bold; }
32
			header nav span.title { font-weight: bold; }
33
  
33
  
34
			section { margin-block-start: 3em; margin-block-end: 3em; }
34
			section { margin-block-start: 3em; margin-block-end: 3em; }
35
  
35
			blockquote { border-left: 0.2em solid black; padding-left: 1em; margin-left: 0; }
36
			blockquote {
  
37
				border-left: 0.3em solid black;
  
38
				padding-left: 1em;
  
39
				margin-left: 0;
  
40
			}
  
41
  
  
42
			footer { font-size: small; }
36
			footer { font-size: small; }
43
  
37
  
44
			ul.post-list { padding: 0em; }
38
			ul.post-list { padding: 0em; }
...
53
			article h1 { font-size: 130%; line-height: 110%; }
47
			article h1 { font-size: 130%; line-height: 110%; }
54
			article code { background: lemonchiffon; padding: 0 0.2em; }
48
			article code { background: lemonchiffon; padding: 0 0.2em; }
55
			article pre { border: 1px solid var(--border-color); padding: 1em; line-height: 140%; text-wrap: nowrap; overflow-x: auto; }
49
			article pre { border: 1px solid var(--border-color); padding: 1em; line-height: 140%; text-wrap: nowrap; overflow-x: auto; }
56
			article pre > code { background: initial; }
50
			article pre > code { background: initial; padding: 0; }
57
  
51
  
58
			img, video, audio { max-width: 100%; }
52
			img, video, audio { max-width: 100%; }
59
			figure { display: flex; justify-content: center; margin: 1.5em 0; }
53
			figure { display: flex; justify-content: center; margin: 1.5em 0; }
...