aboutsummaryrefslogtreecommitdiff
path: root/content/posts/2026-01-09-vim.md
blob: cd3cea74e29d24aa6399a8b1642072e802489563 (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
---
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>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.

- `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+v` - launches DebugTerm/DBG debugger with all variables and breaks on main
- `Leader+b` - 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.