aboutsummaryrefslogtreecommitdiff
path: root/content/posts/2026-01-09-vim.md
diff options
context:
space:
mode:
authorMitja Felicijan <mitja.felicijan@gmail.com>2026-01-09 11:29:13 +0100
committerMitja Felicijan <mitja.felicijan@gmail.com>2026-01-09 11:29:13 +0100
commit3d4a24ef30897fdeab06e3fa7da1086e52db641e (patch)
treedc107c1bceea637e4ef7843b7f601073c8b1e150 /content/posts/2026-01-09-vim.md
parent3cebb54fafd69fa0db228d841e381ccca60e39e7 (diff)
downloadmitjafelicijan.com-3d4a24ef30897fdeab06e3fa7da1086e52db641e.tar.gz
New post: Integrated debugging in Vim
Diffstat (limited to 'content/posts/2026-01-09-vim.md')
-rw-r--r--content/posts/2026-01-09-vim.md204
1 files changed, 204 insertions, 0 deletions
diff --git a/content/posts/2026-01-09-vim.md b/content/posts/2026-01-09-vim.md
new file mode 100644
index 0000000..16147db
--- /dev/null
+++ b/content/posts/2026-01-09-vim.md
@@ -0,0 +1,204 @@
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>v :call LocalDebugMain()<CR>
130nnoremap <leader>b :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('directory ' . getcwd())
145 call TermDebugSendCommand('break main')
146 call TermDebugSendCommand('run')
147endfunction
148
149function! 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')
159endfunction
160
161function! 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
177endfunction
178```
179
180I am using the CtrlP plugin, so I also use it for displaying the Quickfix List.
181But if the plugin is not found; it will default to native quicklist with
182`:copen`.
183
184Lets 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
191This setup can get even more elaborate. Depending on your needs. This example
192works really well for projects using C, but anything goes here.
193
194## Why use `:term` and `:TermDebug`
195
196It's very easy to yank and paste from internal terminal buffers even if you are
197not using `tmux` as multiplexer. Just makes the whole thing much easier. I don
198however use `tmux` as well but for compile/debug loop this proved to be a much
199better experience.
200
201`:TermDebug` is also a no brainier. The integration of GDB directly in Vim
202makes adding new breakpoints with `:Break` just seamless. This goes for all
203other commands as well. You can read more about other commands with `:h
204Termdebug` or on https://vimhelp.org/terminal.txt.html.