aboutsummaryrefslogtreecommitdiff
path: root/content/posts/2026-01-09-vim.md
blob: 0c965c84b6abffdf554caa1bb615631508e71fd5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
---
title: Integrated GDB Debugging in Vim with TermDebug
url: integrated-gdb-debugging-in-vim-with-termdebug.html
date: 2026-01-09T16:13:13+02:00
type: post
draft: false
tags: []
---

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

<video width="100%" controls>
  <source src="/assets/posts/vim-gdb/demo.mp4" type="video/mp4">
</video>

## Main `~/.vimrc`

My `~/.vimrc` is very minimal. I only use these four plugins:

- https://github.com/tpope/vim-commentary - comment stuff out
- https://github.com/ctrlpvim/ctrlp.vim - fuzzy file, buffer finder
- https://github.com/dense-analysis/ale - LSP integration
- https://github.com/mitjafelicijan/sniper.vim - buffer bookmark manager

At the top of my configuration file I have the following.

```vimrc
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](https://www.gnu.org/software/make/) and [Clang](https://clang.llvm.org/).

Project structure

```
~/Projects/cproject
    main.c
    Makefile
    .vimrc
```

```Makefile
# Makefile
cprogram: main.c
    clang -g -o cprogram main.c
```

```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;
}
```

```vim
" .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>m :call LocalDebugMain()<CR>
nnoremap <leader>l :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('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
	for [k, v] in items(g:_envs)
		execute printf("let $%s = %s", k, string(v))
	endfor

	silent make

	" Filter non valid errors out of quicklist.
	let qfl = getqflist()
	let filtered = filter(copy(qfl), {_, entry -> entry.valid == 1})
	call setqflist(filtered, 'r')

	redraw!
	execute len(filtered) > 0 ? 'copen' : 'cclose'
endfunction
```

> **Note**: If any errors are found during the compilation mode the Quickfix
> list will be populated and opened. You could use CtrlP Quickfix with
> `:CtrlPQuickfix` instead of `:copen` for this as well, I just want to stick
> to what Vim supports natively.

Lets check these keybindings.

- `Leader+x` - runs the binary in the terminal above and providing environment variables and argument
- `Leader+c` - runs make and puts error in Quickfix List then opens with `:copen`
- `Leader+m` - launches DebugTerm/DBG debugger with all variables and breaks on main
- `Leader+l` - 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.

## Isn't this setup a bit elaborate?

Yes and no. I don't change these files much at all. When I start a new C
project I just copy the existing one and call it a day. And any significantly
complex project will require certain per project specific configurations
anyway.

This also makes it shareable with other people without dumping your whole setup
on them. There are drawback like with anything else, but I have found this
approach to be the lesser of two evils kinda think. Even Vim users can ignore
these files by not enabling local configuration files.

Like any other tool, it's on you to make your development workflow as
frictionless as possible and no tool is perfect.