DEV Community

Igor Irianto
Igor Irianto

Posted on • Updated on

Debugging in Vim with Vimspector

Follow @learnvim for more Vim tips and tricks!

Debugging in Vim with Vimspector

Vimspector is a powerful graphical debugger plugin for Vim. However, it also will take you a while to get started. In this article, I will show you how to use Vimspector to debug:

  • A Node file.
  • Client-side app using Chrome.
  • Jest testing.
  • An Express app.

Both their Github page and their website are very comprehensive. I would suggest you read them when you have the chance!

Much content found in this article can be found inside the Vimspector websites. However, when I was reading them, I was overwhelmed by the sheer amount of the information. This article serves as a bridge to get you started as early as possible. I think it is beneficial to get our hands dirty early-on. Once you start using it, reading the docs becomes clearer. Although I am using the Javascript ecosystem as examples (backend, frontend, testing), you should be able to apply the same concept to any language of your choice.

Requirements

At the time of this writing, I am using Vim 8.2 on MacOS Catalina. This guide should work with any OS (with appropriate modifications). The Vimspector Github page though, recommends you to have either Vim 8.2 or NeoVim 0.4.3. You also need to have Python 3.6 or up.

Quick Overview

So what is Vimspector? How does it work?

Vimspector is not a universal language debugger. It technically does not handle the debugging. Think of it like a middle-man that facilitates communicating with the debugger for your language-of-choice. It relies on specific gadgets depending on what language you are working with. So if you want to debug a node application, use the vscode-node-debug2 gadget. If you want to debug a go app, use vscode-go gadget. If you want to debug a client-side JS app, use debugger-for-chrome gadget.

Different languages have different debuggers. Some have built-in debuggers while some use external libraries for debugging. If we want to integrate multiple languages into our favorite editors / IDEs, it could get messy. A Python debugger communicates differently from a Node debugger. A Node debugger behaves differently from a front-end JS debugger via Chrome. If you're a Ruby / Javascript developer, you could technically have a Ruby and Node debuggers installed in your editor / IDE. But these two debuggers probably have different protocols and exhibit different behaviors. The more languages / environments you deal with, the more the complexity increases.

So having different language environments to debug can get messy. To reconcile these differences, we need to bridge the gap between language X debugger and our editor / IDE - we need an abstract protocol. This protocol is known as Debug Adapter Protocol (DAP). DAP was originally created by the VSCode team. But luckily for us, they decided to make it (somewhat) editor agnostic, so developers can use it for other editors / IDEs. Vimspector is the result of DAP development for the Vim editor. For a list of tools that support DAP, check out this page: Tools Supporting the DAP.

Although not required readings, but when you have the time, I would strongly recommend you to look into these pages to get a better understanding on DAP:

Well, enough theory - let's get started with an actual example!

Getting started

To get started, install Vimspector if you haven't. Follow the Vimspector installation guide.

For me, I used vim-plug. All I did was add the following in my vimrc's list of plugins:

Plug 'puremourning/vimspector'
Enter fullscreen mode Exit fullscreen mode

I then sourced my vimrc and ran :PlugInstall.

Examples

The fastest way to learn a new skill is to jump into it and learn it along the way. You may have a lot of questions right now, but once you see how it works, by the end of this article, I hope that some of your questions will be answered.

Let's go through the node example.

Debugging a node app

The Vimspector plugin itself already comes with examples. Go to the directory where Vim saves your Vimspector plugin. I am using vim-plugged, so my plugins are installed inside the plugged/ directory. In my case, the directory is in ~/.vim/plugged/vimspector/. Yours might be in a different location, depending on your plugin manager and your system. Once you find them, from the vimspector/ directory, go to the /support/test/ directory. Inside this directory, you will find different examples that I also strongly encourage you to check out once you're done reading this article.

Since this section is about debugging a node app, let's check out the node directory.

cd node/
Enter fullscreen mode Exit fullscreen mode

Inside, at the time of this writing, you should find a directory named simple/. Go there and you'll see a simple.js file.

Before we debug this file, there are at least two requirements to debug with Vimspector:

  1. A relevant gadget.
  2. A Vimspector config file.

A gadget is a debug adapter (like the ones listed in Microsoft's Debug Adapters page). Since we are debugging a node app, we need a node adapter. This node adapter will relay messages between NodeJS and the abstract protocol DAP.

(By the way, before installing the node adapter, per Vimspector's site, you need to be using a Node version between 6 and 12).

For the first requirement, to install a node adapter, from Vim, run :VimspectorInstall vscode-node-debug2.You need a different gadget when you are debugging a different language/environment. If you need to debug a Python file, you have to install a python gadget. If you are debugging a Go file, you have to install a Go gadget. Since we are debugging a node app, we need to install a node gadget. For a list of gadgets, check out this section from the Vimspector Github page: Supported Languages.

For the second requirement, the .vimspector.json file is conveniently already located at the root of the project (inside simple/). If you check the hidden files of the vimspector/support/test/simple/ directory, there should already be one .vimspector.json file available, so you don't need to do anything. Keep in mind that when you are inspecting your own project, remember to create your own vimspector file.

With those two requirements in place, let's debug.

Vimspector offers many commands and shortcuts. Using them all when you're starting can be overwhelming. I found that the following are sufficient to get started with. Below are some of my vimrc vimspector shortcuts:

nnoremap <Leader>dd :call vimspector#Launch()<CR>
nnoremap <Leader>de :call vimspector#Reset()<CR>
nnoremap <Leader>dc :call vimspector#Continue()<CR>

nnoremap <Leader>dt :call vimspector#ToggleBreakpoint()<CR>
nnoremap <Leader>dT :call vimspector#ClearBreakpoints()<CR>

nmap <Leader>dk <Plug>VimspectorRestart
nmap <Leader>dh <Plug>VimspectorStepOut
nmap <Leader>dl <Plug>VimspectorStepInto
nmap <Leader>dj <Plug>VimspectorStepOver
Enter fullscreen mode Exit fullscreen mode

Feel free to steal what I have or make your own. Vimspector also comes with a set of shortcuts called the human-mode mapping. If you're used to VSCode debugging shortcut, you may feel more at home with them.

Finally, let's open simple.js. It should look something like this:

var msg = 'Hello, world!'

var obj = {
  test: 'testing',
  toast: function() {
    return 'toasty' + this.test;
  }
}

console.log( "OK stuff happened " + obj.toast() )
Enter fullscreen mode Exit fullscreen mode

To launch vimspector, run the launch command. Press <Leader>dd (:call vimsepctor#Launch()). You should see a Vimspector window. Pretty cool!

Vimspector layout

There should be 6 different windows displayed - depending on your Vim orientation, you may see them in different order. If you have never used a debugger before, don't feel intimidated. You'll get used to some of them after playing with them for a while. To exit Vimspector, press <Leader>de (:call vimspector#Reset()).

By the way, there will be some prompts on the bottom of the windows when you launch vimspector, asking for something like ...Break on Uncaught Exceptions?. I usually pressed N. If you don't want to get prompted all the time, add these in your vimspector.json file inside the "run": { ... block:

  ...
  "breakpoints": {
    "exception": {
      "all": "N",
      "uncaught": "N"
    }
  },
  ...
Enter fullscreen mode Exit fullscreen mode

My full vimspector json (so you can just copy paste it) for this file looks like this:

{
  "configurations": {
    "run": {
      "adapter": "vscode-node",
      "breakpoints": {
        "exception": {
          "all": "N",
          "uncaught": "N"
        }
      },
      "configuration": {
        "request": "launch",
        "protocol": "auto",
        "stopOnEntry": true,
        "console": "integratedTerminal",
        "program": "${workspaceRoot}/simple.js",
        "cwd": "${workspaceRoot}"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Back to the debugging, start the vimspector again (<Leader>dd). Since we have stepOnEntry to be true in the Vimspector json file, Vimspector will stop on the first line even though you haven't marked a breakpoint.

To traverse through the file, you can either:

  1. Step out (steps out of the scope)
  2. Step into (steps into the function scope)
  3. Step over (steps to the next line, in scope)

Here are the key maps that I use. Note that I use hlj keys similar to the Vim movement keys.

nmap <Leader>dh <Plug>VimspectorStepOut
nmap <Leader>dl <Plug>VimspectorStepInto
nmap <Leader>dj <Plug>VimspectorStepOver
Enter fullscreen mode Exit fullscreen mode

If you are not sure what stepping out, stepping into, and stepping over are, I found these short video tutorials very helpful:

Back to our debugging - now that we are on the first line of the code, press <Leader>dj (<Plug>VimspectorStepOver). Note the highlight moves to the variable declaration. If you press <Leader>dj one more time, it will move down again. If you keep stepping over, eventually you will reach the end of simple.js file. Unfortunately, you can't step "back" to the previous line. Once you're on the next step, you continue forward until you reach the end.

If you accidentally step over an important line, just restart the debugger. To restart, run <Leader>dk (<Plug>VimspectorRestart). When you restart, Vimspector starts over from the beginning. Alternatively, you could also Reset and Launch Vimspector again.

You can put breakpoints throughout the file. Back in the main simple.js file, run <Leader>dt (:call vimspector#ToggleBreakpoint()) on the line where you want to add a breakpoint (run that command again on the line with the breakpoint to remove it).

Once you sprinkle breakpoints all over your file, launch Vimspector again. If you press <Leader>dc (:call vimspector#Continue()), Vimspector will jump to the next breakpoint. Pretty cool!

Vimspector continue

To clear the breakpoints, run <Leader>dT (:call vimspector#ClearBreakpoints()).

Before moving on to the next section, spend 10-15 minutes experimenting with simple.js. Change the codes in simple.js. Move around. Play.

Before we move on to the next section, let's briefly go over what the 6 Vimspector windows do.

Variables window

The Variables window contains the available variables (and their current values) relative to their current scope.

- Scope: Local
  + this (Object): Object
  - __dirname (string): "/Users/iggy/.vim/plugged/vimspector/support/test/node/simple"
  - __filename (string): "/Users/iggy/.vim/plugged/vimspector/support/test/node/simple/simple.js"
  + exports (Object): Object {}
  + module (Object): Module {id: ".", path: "/Users/iggy/.vim/plugged/vimspector/support/test/n…", exports: Object, …}
 *- msg (undefined): undefined
 *- obj (undefined): undefined
  + require (Function): function require(path) { … }
+ Scope: Global
Enter fullscreen mode Exit fullscreen mode

In the above example, inside the Local scope, I have common Node variables like this, __dirname, __filename and the written variables msg and obj. Pay attention as you step over to the next variable. Watch them go from undefined to having a value.

Do this: step over and into different function scopes. Find a way to get inside a function and see what the available variables are. Then step outside and compare them. Also check what is inside Scope: Global. Why does it have the variables it has? What does it tell you about Node?

Watch Window

The Watch window is where you can watch for specific values. Initially it will be blank. If you want to watch the value of the msg variable, type into the Watch window that variable, msg. When you're at the start of the file, the value will be undefined. Then as you steps over, the value will change into 'Hello, world!'.

Stack Trace window

The Stack Trace window displays the call stack of the node file execution.

Console window

In the Console window, you can enter the defined variables like msg. You can also evaluate expressions.

Terminal window

The Terminal window displays all the outputs throughout the entire debugging session.

Debugging in a Browser

Let's explore how to use the Chrome-debugger to debug a client-side app. Lucky for us, there is also an example in the Vimspector directory. Inside ~/.vim/plugged/vimspector/support/test/chrome/ directory, you will find a file named run_server, a test.js, and a .vimspector.json.

Let's check out what is inside the vimspector config file:

{
  "configurations": {
    "launch": {
      "adapter": "chrome",
      "configuration": {
        "request": "launch",
        "url": "http://localhost:1234/",
        "webRoot": "${workspaceRoot}/www"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This configuration sure looks different from the Node Vimspector config one we saw earlier. One important line is "adapter": "chrome" - it indicates that we will be using a Chrome adapter. The URL is defined to be on localhost:1234 because that's the port where our server will be running on.

Btw, when you launch Vimspector later, it will prompt if you want to break on uncaught exceptions etc again. If you don't want to deal with those prompts, add these lines inside the .vimspector.json just like you added it to your Node app earlier.

"breakpoints": {
  "exception": {
    "all": "N",
    "uncaught": "N"
  }
},
Enter fullscreen mode Exit fullscreen mode

We need to install the correct gadget. In the earlier section, we installed one with :VimspectorInstall vscode-node-debug2. This time we have to install one for chrome. Run this from Vim:

:VimspectorInstall debugger-for-chrome
Enter fullscreen mode Exit fullscreen mode

Once done, run the server (make sure you have PHP installed):

./run_server
Enter fullscreen mode Exit fullscreen mode

Check out localhost:1234. You should see a simple app with some pop-out modals.

Now open www/js/test.js. Add breakpoints anywhere you like. Run the Vimspector launch command <Leader>dd. By running it, it will automatically launch the Chrome browser. Vimspector will pause the Chrome browser where your breakpoints are. Step over and step into your breakpoints. Watch some variables. Change the code. Play around!

Note: check out the Variables window. Check out both the Local and Global scope. Did you see anything different in the Global scope compared to when you're debugging with Node? What does this tell you about client-side vs backend code execution?

Note 2: notice how this time, when you launch Vimspector, it launches a Chrome browser. If you look at the .vimspector.json, you'll see a "request": "launch", instead of "request": "attach", from earlier Node debugging. How are they different? There are two ways you can debug an app: by attaching it to an already running process or by launching a new process.

Debugging a Jest test

Now let's learn how to debug a Jest test. For this section, I will be using the vscode-recipes repository. First go to the site and clone the repository. Then go to the debugging-jest-tests/ directory. You will find two directories: lib/ and a test/. This is our workspace root directory.

First, install the dependencies: npm i.

Make sure that the Jest test is running and they are all passing: npm run test.

Debugging a Jest test requires a node debugger. If you've been typing along, you should already have the Node debugger from earlier. With Vimspector, you install your gadget only once (Vimspector saves all the installed gadgets inside the Vimspector directory - in my case, they are stored inside ~/.vim/plugged/vimspector/).

Inside the debugging-jest-tests/ directory, add a .vimspector.json file. Inside it:

  "configurations": {
    "my awesome jest test": {
      "adapter": "vscode-node",
      "breakpoints": {
        "exception": {
          "all": "N",
          "uncaught": "N"
        }
      },
      "configuration": {
        "request": "launch",
        "name": "Jest debugger",
        "type": "node",
        "console": "integratedTerminal",
        "program": "${workspaceRoot}/node_modules/.bin/jest",
        "skipFiles": ["*/<node_internals>/**/*.js", "node_modules/**/*.js"],
        "cwd": "${workspaceRoot}"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The configuration looks familiar, with a new addition of: "program": "${workspaceRoot}/node_modules/.bin/jest". Here's the breakdown:

  • The workspaceRoot is the current directory (the debugging-jest-tests/ directory).
  • The node_modules/.bin/jest is the Jest executable from the node_modules/ (which you should have after npm i).

This time when you run the debugger, Vimspector needs to run the Jest executable. One place where you can find a Jest executable is inside the node_modules/ directory. You could've also run it from the global Jest command, but I like to keep it compartmentalized (what if I am running this from a container and I'm not guaranteed to have a global Jest command? By using the .bin/jest command, I'm guaranteed to have it - but that's just my personal preference).

Cool! Let's put some breakpoints inside the test files, then launch the Vimspector (<Leader>dd). Voila! Your test suite will pause and you can now debug your code.

If you put your breakpoint on the line where it calls the function, like the function add() below:

...
it('Should return correct result', () => {
  const result = add(1, 2); // put a breakpoint here
  expect(result).toEqual(3);
});
...
Enter fullscreen mode Exit fullscreen mode

When you step into it (<Leader>dl), it will go into the original function declaration inside lib/calc.js, allowing you to investigate the source code. How awesome is that!? With this, you can debug a faulty test down to where the function originated!

There is one problem. With the current Vimspector config, it will run all the tests when you launch it. That's great but in real life, your app probably has hundreds of tests (if you've been practicing TDD... wink wink). Running all tests are probably not the best way to live your life. What if you want to run one particular test at a time?

You sure can!

In Jest, you can run a specific file by passing that file name (or part of the name) as an argument. If you want to run only the add.spec.js, you can run the command jest add. Jest is smart enough to match the add.spec.js and not the subtract.spec.js.

If you want to match a particular test in a file, Jest is also smart enough to match any keyword you pass using the -t option.

Suppose that inside the add.spec.js I have two tests (notice that I modified the test descriptions):

const { add } = require('../lib/calc');

describe('When adding numbers', () => {
  it('one should return correct result', () => {
    const result = add(1, 2);
    expect(result).toEqual(3);
  });

  it('two should not return correct result', () => {
    const result = add(1, 5);
    expect(result).not.toEqual(3);
  });
});
Enter fullscreen mode Exit fullscreen mode

I want to run only the second test. To do that, from the CLI I can run test add -t two. Jest is smart enough to run only add.spec.js and only the 'two should not return correct result' test! Try it.

Armed with that knowledge, we need to pass these arguments when running Vimspector. Turns out that Vimspector has the args attribute where you can pass argument(s) to your program.

{
  "configurations": {
    "jest": {
      "adapter": "vscode-node",
      "breakpoints": {
        "exception": {
          "all": "N",
          "uncaught": "N"
        }
      },
      "configuration": {
        "request": "launch",
        "name": "Jest debugger",
        "type": "node",
        "console": "integratedTerminal",
        "program": "${workspaceRoot}/node_modules/.bin/jest",
        "skipFiles": ["*/<node_internals>/**/*.js", "node_modules/**/*.js"],
        "cwd": "${workspaceRoot}",
        "args": [
                "${FileName}",
                "-t",
                "${TestName}"
            ]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Our new attribute, args, is an array with 3 elements. The funny looking variables (${FileName} and ${TestName}) are the Vimspector's named arguments. With this I can pass the FileName and TestName variables when launching Vimspector.

Let's launch Vimspector again. This time it will prompt: "Enter value for FileName" (in which you'll enter "add"). After that, the Vimspector will prompt: "Enter value for TestName" (in which you'll enter "two"). And watch it runs only that particular test from that particular file. Success!!

Now you have no excuse not to practice TDD! :D

Debugging an Express App

For the next example, let's try to debug a simple Express app. Since express is a node library, a node gadget is required.

Create a directory (mkdir express-debug) and go in there. Run npm init -y to initialize an NPM project. Install express (npm i express). Then create an app.js. Inside it:

const express = require('express');
const app = express();
const port = 3000;

const helloFunc = () => {
  const hello = 'hello';
  return hello;
};

app.get('/', (req, res) => {
  const msg = helloFunc();
  res.send(msg);
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}!`)
});

Enter fullscreen mode Exit fullscreen mode

Create a .vimspector.json in that directory. At minimum you should have:

{
  "configurations": {
    "run": {
      "adapter": "vscode-node",
      "default": true,
      "configuration": {
        "type": "node",
        "request": "attach",
        "processId": "${processId}"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

However, it's probably better if we enable some default configs:

{
  "configurations": {
    "run": {
      "adapter": "vscode-node",
      "default": true,
      "breakpoints": {
        "exception": {
          "all": "N",
          "uncaught": "N"
        }
      },
      "configuration": {
        "name": "Attaching to a process ID",
        "type": "node",
        "request": "attach",
        "skipFiles": ["node_modules/**/*.js", "<node_internals>/**/*.js"],
        "processId": "${processId}"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Then run the express app in inspect mode:

node --inspect app.js
Enter fullscreen mode Exit fullscreen mode

You should see your app running on localhost:3000. Next, add a few breakpoints inside app.js, then launch Vimspector (it will also ask for processId, but I find that not giving it any value and simply pressing the Return/Enter key works).

Finally, refresh the page and you should see the debugger pauses at your first breakpoint. From there, you can step out, step into, and step over your code.

Vimspector express

Congratulations! You've successfully debugged an express app.

What's next?

This article barely scratched the surface of what a debugger can do. There are many more things that you can do with Vimspector. Hopefully by approaching it from different angles, you'll gain a more thorough understanding of this plugin.

Your debugging needs probably differ from the examples I gave in this article, but I am a sincere believer that if you understand the principles behind it, you should be able to implement Vimspector to meet your needs.

With that, thanks for reading this far. Happy Vimming!

Discussion (2)

Collapse
phantas0s profile image
Matthieu Cneude

Super nice and detailed overview, thanks for that!

I'm using it for a couple of days now and it's really awesome. It supports so many languages it's great!

Collapse
iggredible profile image
Igor Irianto Author

Thank you for the kind reply, appreciate it. There is so much in this plugin that I still haven't grasped, but this plugin definitely has a lot of untapped potential!