← Back to DevBytes

Vim Debugging: Complete Guide

Introduction to Vim Debugging

Debugging is one of the most critical skills in a developer's toolkit, and doing it efficiently inside Vim can dramatically improve your workflow. Vim debugging refers to the set of techniques, plugins, and built-in features that allow you to inspect, step through, and diagnose code directly from the Vim editor. Rather than switching between your editor and an external debugger, Vim lets you stay in a single environment, preserving your context and momentum.

Whether you are chasing a segmentation fault in C, a runtime error in Python, or a logic bug in JavaScript, Vim offers multiple paths to find and fix issues. This guide covers everything from built-in debugging tools like vim -D and the ex debug mode, to modern plugin-based solutions like vimspector and DAP (Debug Adapter Protocol) integration.

Why Vim Debugging Matters

Context switching is expensive. Every time you leave your editor to fire up a separate debugger, you lose mental state. Vim debugging matters because it keeps you in the environment where you write, navigate, and understand your code. Here are the key reasons to master it:

Built-in Vim Debugging Tools

Debugging Vim Itself with vim -D

Before diving into debugging your code, it is worth knowing that Vim has a built-in debug mode for debugging Vim scripts and plugins. Launch Vim with the -D flag to enter debug mode:

vim -D myfile.vim

This starts Vim in debug mode, where you can step through script execution line by line. Once in debug mode, you will see a prompt like > where you can enter debug commands:

" Step to the next line
> step

" Continue execution until a breakpoint
> cont

" Set a breakpoint at a specific line in a file
> breakadd file 42 ~/.vim/plugin/myplugin.vim

" List all breakpoints
> breaklist

" Remove a breakpoint
> breakdel file 42 ~/.vim/plugin/myplugin.vim

" Print the value of a variable
> echo g:my_variable

" Evaluate an expression
> eval len(g:my_list)

This is particularly useful when a plugin misbehaves or when you are writing your own Vimscript and need to understand why something is not working as expected.

Debugging Vimscript Functions

You can also debug Vimscript functions at runtime. Use :breakadd to set breakpoints on functions:

" Break when a specific function is called
:breakadd func MyFunction

" Break at a specific line within a function
:breakadd func 10 MyFunction

" Break inside a sourced script
:breakadd file 25 ~/.vimrc

" Resume normal execution
:cont

When a breakpoint is hit, Vim drops you into the debug prompt, where you can inspect variables, step through lines, and understand the execution flow.

Debugging Your Code with Terminals

Using the Built-in Terminal

Vim 8 and later includes a built-in terminal emulator. This is the simplest way to debug without plugins. You can open a terminal in a split window and run your debugger of choice:

" Open a terminal in a horizontal split
:terminal

" Or use the shorthand
:term

" Open a terminal in a vertical split
:vert term

" Run gdb directly
:term gdb ./myprogram

For example, to debug a C program with GDB inside Vim:

" Compile with debug symbols
:term gcc -g -o myprogram myprogram.c

" Start GDB in a vertical split
:vert term gdb ./myprogram

You can then use GDB commands in the terminal pane while viewing your source code in the other pane. This is a lightweight approach that works with any command-line debugger: GDB, LLDB, PDB, IPDB, Node inspect, and more.

Termdebug: Vim's Built-in GDB Integration

Vim ships with a plugin called termdebug that provides a more integrated GDB experience. It is bundled with Vim but needs to be loaded explicitly:

" Load the termdebug plugin
:packadd termdebug

" Start debugging a program
:Termdebug ./myprogram

This opens three windows: your source code, a GDB terminal, and a program output terminal. Key features include:

Useful termdebug commands:

" Set a breakpoint at the current line
:Break

" Delete a breakpoint at the current line
:Clear

" Step into the next line
:Step

" Step over the next line
:Next

" Step out of the current function
:Finish

" Continue execution
:Continue

" Evaluate an expression under the cursor
:Evaluate

" Stop debugging
:Stop

To make termdebug load automatically, add this to your .vimrc:

" Enable termdebug by default
packadd termdebug

" Optional: use vertical splits for the program window
let g:termdebug_wide = 1

Modern Plugin-Based Debugging with Vimspector

What is Vimspector?

Vimspector is a powerful debugging plugin for Vim that implements the Debug Adapter Protocol (DAP). This is the same protocol used by VS Code, which means it supports a wide range of languages including Python, JavaScript, TypeScript, C/C++, Go, Rust, Java, and more. Vimspector provides a unified, feature-rich debugging experience inside Vim.

Installing Vimspector

Install Vimspector using your preferred plugin manager. Here is an example with vim-plug:

" In your .vimrc
call plug#begin('~/.vim/plugged')
Plug 'puremourning/vimspector'
call plug#end()

Then run the install command in Vim:

:PlugInstall

After installation, you need to install the debug adapters for the languages you want to debug:

:VimspectorInstall

This opens an interactive menu where you can select which debug adapters to install, such as debugpy for Python, vscode-cpptools for C/C++, or vscode-node-debug2 for Node.js.

Configuring Vimspector

Vimspector uses a JSON configuration file called .vimspector.json placed in your project root. Here is an example for a Python project:

{
  "configurations": {
    "Run - Current File": {
      "adapter": "debugpy",
      "configuration": {
        "request": "launch",
        "program": "${file}",
        "cwd": "${workspaceRoot}",
        "stopOnEntry": false,
        "console": "integratedTerminal"
      }
    },
    "Attach - Remote": {
      "adapter": "debugpy",
      "configuration": {
        "request": "attach",
        "host": "localhost",
        "port": 5678,
        "pathMappings": [
          {
            "localRoot": "${workspaceRoot}",
            "remoteRoot": "/app"
          }
        ]
      }
    }
  }
}

Here is an example configuration for a C/C++ program using GDB:

{
  "configurations": {
    "Launch": {
      "adapter": "vscode-cpptools",
      "configuration": {
        "request": "launch",
        "program": "${workspaceRoot}/build/myprogram",
        "args": [],
        "cwd": "${workspaceRoot}",
        "environment": [],
        "externalConsole": false,
        "MIMode": "gdb",
        "miDebuggerPath": "/usr/bin/gdb",
        "stopAtEntry": false
      },
      "breakpoints": {
        "exception": {
          "caught": "N",
          "uncaught": "N"
        }
      }
    }
  }
}

And here is a configuration for debugging a Node.js application:

{
  "configurations": {
    "Launch": {
      "adapter": "vscode-node-debug2",
      "configuration": {
        "request": "launch",
        "program": "${workspaceRoot}/src/index.js",
        "cwd": "${workspaceRoot}",
        "stopOnEntry": false
      }
    }
  }
}

Using Vimspector

Once your configuration is in place, start debugging with the following commands:

" Start debugging (prompts you to choose a configuration)
:VimspectorContinue

" Or launch a specific configuration
:call vimspector#LaunchWithSettings({"configuration": "Run - Current File"})

Vimspector provides a rich set of default key mappings. Here are the most important ones:

" Start / Continue debugging
F5

" Step over
<F10>  or  <Leader>dO

" Step into
<F11>  or  <Leader>dI

" Step out
<F12>  or  <Leader>d<F12>

" Toggle breakpoint at current line
<Leader>db

" Toggle a conditional breakpoint
<Leader>dB

" Clear all breakpoints
<Leader>dc

" Pause execution
<Leader>dp

" Restart debugging
<Leader>dr

" Stop debugging and close
<Leader>de

" Add a watch expression
<Leader>dw

" Evaluate expression under cursor
<Leader>di

You can also customize these mappings in your .vimrc:

let g:vimspector_enable_mappings = 'HUMAN'

" Or define your own mappings
nmap <Leader>dd <Plug>VimspectorContinue
nmap <Leader>ds <Plug>VimspectorStepOver
nmap <Leader>di <Plug>VimspectorStepInto
nmap <Leader>do <Plug>VimspectorStepOut
nmap <Leader>dt <Plug>VimspectorToggleBreakpoint
nmap <Leader>dT <Plug>VimspectorToggleConditionalBreakpoint
nmap <Leader>dx <Plug>VimspectorStop
nmap <Leader>dr <Plug>VimspectorRestart

The Vimspector UI

When you start a debugging session, Vimspector opens several windows:

You can navigate between these windows using standard Vim window commands like <C-w>h, <C-w>j, <C-w>k, and <C-w>l.

Language-Specific Debugging Approaches

Python Debugging

For Python, you can use the built-in pdb module directly in your code:

import pdb

def calculate_total(items):
    total = 0
    for item in items:
        pdb.set_trace()  # Execution pauses here
        total += item.price
    return total

For a more integrated experience, use debugpy with Vimspector. First, install it:

pip install debugpy

For remote debugging, add this to your Python code:

import debugpy
debugpy.listen(('0.0.0.0', 5678))
print("Waiting for debugger to attach...")
debugpy.wait_for_client()

Then use the "Attach - Remote" configuration from the Vimspector JSON example above to connect from Vim.

C and C++ Debugging

For C/C++, compile your code with debug symbols using the -g flag:

gcc -g -O0 -o myprogram myprogram.c
# or with make
make CFLAGS="-g -O0"

Use -O0 to disable optimizations, which can reorder code and make stepping confusing. You can then use either termdebug or Vimspector with the vscode-cpptools adapter.

For core dump analysis:

" Open the core dump in GDB via termdebug
:TermdebugCommand gdb ./myprogram core.12345

JavaScript and Node.js Debugging

For Node.js, you can use the built-in inspector. Start your application with the inspect flag:

node --inspect-brk src/index.js

The --inspect-brk flag pauses execution on the first line, waiting for a debugger to attach. Use the Vimspector Node.js configuration to connect.

For browser JavaScript, you can use the Chrome Debug Adapter:

{
  "configurations": {
    "Launch Chrome": {
      "adapter": "chrome",
      "configuration": {
        "request": "launch",
        "url": "http://localhost:3000",
        "webRoot": "${workspaceRoot}/src"
      }
    }
  }
}

Go Debugging

For Go, install delve, the Go debugger:

go install github.com/go-delve/delve/cmd/dlv@latest

Then use this Vimspector configuration:

{
  "configurations": {
    "Launch": {
      "adapter": "delve",
      "configuration": {
        "request": "launch",
        "program": "${workspaceRoot}",
        "mode": "debug",
        "cwd": "${workspaceRoot}"
      }
    }
  }
}

Advanced Debugging Techniques

Conditional Breakpoints

Conditional breakpoints only pause execution when a specific condition is met. This is invaluable for debugging loops or frequently called functions:

" In termdebug, set a conditional breakpoint in GDB
(gdb) break myfunction if x > 100

" In Vimspector, use the conditional breakpoint mapping
<Leader>dB

When prompted, enter your condition, for example i == 42 or len(my_list) > 1000.

Logpoints (Tracepoints)

Logpoints allow you to log messages without pausing execution. In Vimspector, you can set a logpoint by creating a breakpoint with a log message instead of a pause action. This is useful for understanding execution flow in production-like scenarios where stopping the program is not feasible.

Watch Expressions

Watch expressions let you monitor the value of variables or expressions as they change during execution. In Vimspector:

" Add a watch expression
<Leader>dw

" Then type your expression, e.g.:
len(my_list)

" Or a more complex expression
[x.name for x in items if x.active]

Remote Debugging

Remote debugging is essential when your code runs on a different machine, such as a Docker container or a production server. The general pattern is:

  1. Start the debug server on the remote machine.
  2. Configure Vimspector to attach to the remote debug server.
  3. Set breakpoints in your local copy of the code.
  4. Use path mappings to map local files to remote paths.

Example for debugging inside a Docker container:

{
  "configurations": {
    "Attach to Docker": {
      "adapter": "debugpy",
      "configuration": {
        "request": "attach",
        "host": "localhost",
        "port": 5678,
        "pathMappings": [
          {
            "localRoot": "${workspaceRoot}/app",
            "remoteRoot": "/app"
          }
        ]
      }
    }
  }
}

Best Practices for Vim Debugging

1. Keep Configurations Version-Controlled

Store your .vimspector.json files in your project repository. This ensures every team member has the same debug configurations and can reproduce debugging sessions easily. For project-specific secrets or environment-specific paths, use environment variable substitution:

{
  "configurations": {
    "Launch": {
      "adapter": "debugpy",
      "configuration": {
        "request": "launch",
        "program": "${file}",
        "env": {
          "API_KEY": "${env:API_KEY}",
          "DATABASE_URL": "${env:DATABASE_URL}"
        }
      }
    }
  }
}

2. Use Conditional Breakpoints Wisely

Avoid setting breakpoints inside hot loops without conditions. A breakpoint that fires thousands of times will make debugging painfully slow. Always add conditions to narrow down the exact iteration you care about.

3. Learn Your Debugger's Native Commands

Even when using Vimspector, understanding the underlying debugger (GDB, PDB, Delve) is valuable. Sometimes you need to drop into the native command interface for advanced operations like memory inspection, thread management, or custom commands.

4. Create Custom Mappings That Fit Your Workflow

The default Vimspector mappings may not suit everyone. Create a mapping scheme that feels natural. A common pattern is to use a leader-prefixed namespace:

" Debug leader mappings
let g:vimspector_enable_mappings = 'VISUAL_STUDIO'

" Override specific mappings if needed
nmap <buffer> <Leader>B <Plug>VimspectorBreakpoints
nmap <buffer> <Leader>v <Plug>VimspectorBalloonEval

5. Combine Debugging with Vim's Navigation Features

Vim's quickfix list and location list can be combined with debugging output. For example, you can capture compiler errors or test failures into the quickfix window and jump directly to problematic lines:

" Run tests and populate quickfix
:make test

" Open the quickfix window
:copen

" Jump to the next error
:cnext

6. Use the Terminal for Quick Inspections

For quick, one-off debugging tasks, sometimes the simplest approach is best. Use Vim's terminal to run an interactive REPL or debugger session:

" Open a Python REPL in a split
:vert term python3

" Open an interactive Node.js session
:vert term node

7. Profile Before You Debug

Not all bugs require a debugger. Performance issues are often better diagnosed with profiling tools. Vim can integrate with profilers through the terminal:

" Profile a Python script
:term python3 -m cProfile -o profile.out myscript.py

" Profile a C program with perf
:term perf record ./myprogram
:term perf report

Troubleshooting Common Issues

Vimspector Won't Start

If Vimspector fails to start, check the following:

" Check if the debug adapter is installed
:VimspectorInstall

" View the Vimspector log
:VimspectorShowOutput

" Check for configuration errors
:messages

Common issues include missing debug adapters, incorrect JSON syntax in .vimspector.json, and wrong paths in the configuration.

Breakpoints Not Being Hit

If your breakpoints are not being triggered, verify that:

Termdebug Not Available

If :Termdebug is not recognized, ensure you are running Vim 8.1 or later with the terminal feature enabled:

" Check Vim version and features
:version

" Check if terminal support is compiled in
:echo has('terminal')

If the terminal feature is missing, you may need to install a version of Vim compiled with terminal support, or use Neovim which has terminal support built in.

Neovim-Specific Debugging with nvim-dap

If you use Neovim, the nvim-dap plugin is the Neovim-native equivalent of Vimspector. It also uses the Debug Adapter Protocol and integrates well with Neovim's Lua configuration system.

Install it with your plugin manager:

-- Using packer.nvim
use { 'mfussenegger/nvim-dap' }
use { 'rcarriga/nvim-dap-ui' }  -- Optional UI extension
use { 'theHamsta/nvim-dap-virtual-text' }  -- Optional inline variable display

Basic Lua configuration:

local dap = require('dap')

-- Python configuration
dap.adapters.python = {
  type = 'executable',
  command = 'python',
  args = { '-m', 'debugpy.adapter' },
}

dap.configurations.python = {
  {
    type = 'python',
    request = 'launch',
    name = 'Launch file',
    program = '${file}',
    pythonPath = function()
      return '/usr/bin/python3'
    end,
  },
}

-- Key mappings
vim.keymap.set('n', '<F5>', dap.continue)
vim.keymap.set('n', '<F10>', dap.step_over)
vim.keymap.set('n', '<F11>', dap.step_into)
vim.keymap.set('n', '<F12>', dap.step_out)
vim.keymap.set('n', '<Leader>b', dap.toggle_breakpoint)
vim.keymap.set('n', '<Leader>B', function()
  dap.set_breakpoint(vim.fn.input('Breakpoint condition: '))
end)
vim.keymap.set('n', '<Leader>dr', dap.repl.open)
vim.keymap.set('n', '<Leader>dl', dap.run_last)

The nvim-dap-ui extension provides a rich visual interface similar to Vimspector's, with variable inspection, call stack navigation, and breakpoint management.

Conclusion

Debugging in Vim is not just possible — it can be a deeply efficient and satisfying experience. From the built-in termdebug for GDB integration to the full-featured Vimspector plugin with DAP support, Vim offers debugging capabilities that rival any modern IDE. The key is to choose the right tool for your language and workflow: use termdebug for quick C/C++ sessions, Vimspector or nvim-dap for multi-language projects, and the built-in terminal for lightweight, ad-hoc debugging. By mastering conditional breakpoints, watch expressions, remote debugging, and version-controlled configurations, you can diagnose issues faster and with greater precision. Start with the basics, integrate debugging into your daily workflow, and gradually adopt advanced techniques as you become more comfortable. Your future self, debugging a critical issue at 2 AM, will thank you for the investment.

— Ad —

Google AdSense will appear here after approval

← Back to all articles