diff options
| -rw-r--r-- | content/posts/2026-01-09-vim.md | 204 | ||||
| -rw-r--r-- | static/assets/posts/vim-gdb/demo.mp4 | bin | 0 -> 1466460 bytes | |||
| -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 new file mode 100644 index 0000000..16147db --- /dev/null +++ b/content/posts/2026-01-09-vim.md | |||
| @@ -0,0 +1,204 @@ | |||
| 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 new file mode 100644 index 0000000..33a67c5 --- /dev/null +++ b/static/assets/posts/vim-gdb/demo.mp4 | |||
| Binary files 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 @@ | |||
| 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,7 +47,7 @@ | |||
| 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; } |
