From 3d4a24ef30897fdeab06e3fa7da1086e52db641e Mon Sep 17 00:00:00 2001 From: Mitja Felicijan Date: Fri, 9 Jan 2026 11:29:13 +0100 Subject: New post: Integrated debugging in Vim --- content/posts/2026-01-09-vim.md | 204 +++++++++++++++++++++++++++++++++++ static/assets/posts/vim-gdb/demo.mp4 | Bin 0 -> 1466460 bytes templates/base.html | 10 +- 3 files changed, 206 insertions(+), 8 deletions(-) create mode 100644 content/posts/2026-01-09-vim.md create mode 100644 static/assets/posts/vim-gdb/demo.mp4 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 @@ +--- +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 + + + +## 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 +#include + +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 x :call LocalRun() +nnoremap c :call LocalMake() +nnoremap v :call LocalDebugMain() +nnoremap b :call LocalDebugLine() + +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 don +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. diff --git a/static/assets/posts/vim-gdb/demo.mp4 b/static/assets/posts/vim-gdb/demo.mp4 new file mode 100644 index 0000000..33a67c5 Binary files /dev/null and b/static/assets/posts/vim-gdb/demo.mp4 differ diff --git a/templates/base.html b/templates/base.html index abd16c1..6518c17 100644 --- a/templates/base.html +++ b/templates/base.html @@ -32,13 +32,7 @@ header nav span.title { font-weight: bold; } section { margin-block-start: 3em; margin-block-end: 3em; } - - blockquote { - border-left: 0.3em solid black; - padding-left: 1em; - margin-left: 0; - } - + blockquote { border-left: 0.2em solid black; padding-left: 1em; margin-left: 0; } footer { font-size: small; } ul.post-list { padding: 0em; } @@ -53,7 +47,7 @@ article h1 { font-size: 130%; line-height: 110%; } article code { background: lemonchiffon; padding: 0 0.2em; } article pre { border: 1px solid var(--border-color); padding: 1em; line-height: 140%; text-wrap: nowrap; overflow-x: auto; } - article pre > code { background: initial; } + article pre > code { background: initial; padding: 0; } img, video, audio { max-width: 100%; } figure { display: flex; justify-content: center; margin: 1.5em 0; } -- cgit v1.2.3