This document defines the supported format for project and adapter configuration for Vimspector.

Concepts

As Vimspector supports debugging arbitrary projects, you need to tell it a few details about what you want to debug, and how to go about doing that.

In order to debug things, Vimspector requires a Debug Adapter which bridges between Vimspector and the actual debugger tool. Vimspector can be used with any debug adapter that implements the Debug Adapter Protocol.

For each debugging session, you provide a debug configuration which includes things like:

  • The debug adapter to use (and possibly how to launch and configure it).
  • How to connect to the remote host, if remote debugging.
  • How to launch or attach to your process.

Along with optional additional configuration for things like:

  • Exception breakpoints

Debug adapter configuration

The adapter to use for a particular debug session can be specified inline within the debug configuration, but more usually the debug adapter is defined separately and just referenced from the debug configuration.

The adapter configuration includes things like:

  • How to launch or connect to the debug adapter
  • How to configure it for PID attachment
  • How to set up remote debugging, such as how to launch the process remotely (for example, under gdbserver, ptvsd, etc.)

Debug profile configuration

Projects can have many different debug profiles. For example you might have all of the following, for a given source tree:

  • Remotely launch c++ the process, and break on main
  • Locally Python test and break exception
  • Remotely attach to a c++ process
  • Locally launch a bash script
  • Attach to a JVM listening on a port

Each of these represents a different use case and a different debug configuration. As mentioned above, a debug configuration is essentially:

  • The adapter to use
  • The type of session (launch or attach), and whether or not to do it remotely
  • The configuration to pass to the adapter in order to launch or attach to the process.

The bulk of the configuration is the last of these, which comprises adapter-specific options, as the Debug Adapter Protocol does not specify any standard for launch or attach configuration.

Replacements and variables

Vimspector debug configuration is intended to be as general as possible, and to be committed to source control so that debugging your applications becomes a simple, quick and pain-free habit (e.g. answering questions like “what happens if…” with “just hit F5 and step through!”).

Therefore it’s important to abstract certain details, like runtime and build-time paths, and to parameterise the debug configuration. Vimspector provides a simple mechanism to do this with ${replacement} style replacements.

The values available within the ${...} are defined below, but in summary the following are supported:

  • Environment variables, such as ${PATH}
  • Predefined variables, such as ${workspaceRoot}, ${file} etc.
  • Configuration-defined variables, either provided by the adapter configuration or debug configuration, or from running a simple shell command.
  • Anything else you like - the user will be asked to provide a value.

If the latter 2 are confusing, for now, suffice to say that they are how Vimspector allows parameterisation of debug sessions. The Vimspector website has a good example of where this sort of thing is useful: accepting the name of a test to run.

But for now, consider the following example snippet:

{
  "configurations": {
    "example-debug-configuration": {
      // This is a single-line comment  explaining the purpose
      "adapter": "example-adapter-name",
      "variables": {
        "SecretToken": { // Variables should start with upper-case letters
          "shell" : [ "cat", "${HOME}/.secret_token" ]
        }
      },
      "configuration": {
        "request": "launch" /* or it could be "attach" */,
        "program": [
          "${fileBasenameNoExtension}",
          "-c", "configuration_file.cfg",
          "-u", "${USER}",
          "--test-identifier", "${TestIdentifier}",
          "--secret-token", "${SecretToken}"
        ]
      },
      "breakpoints": {
        "exception": {
          "caught": "",
          "uncaught": "Y"
        }
      }
    }
  }
}

In this (fictitious) example the program launch configuration item contains the following variable substitutions:

  • ${fileBasenameNoExtension} - this is a Predefined Variable, set by Vimspector to the base name of the file that’s opened in Vim, with its extension removed (/path/to/xyz.cc -> xyz).
  • ${USER} - this refers to the Environment Variable USER.
  • ${TestIdentifier} - this variable is not defined, so the user is asked to provide a value interactively when starting debugging. Vimspector remembers what they said and provides it as the default should they debug again.
  • ${SecretToken} - this variable is provided by the configuration’s variables block. Its value is taken from the strip‘d result of running the shell command. Note these variables can be supplied by both the debug and adapter configurations and can be either static strings or shell commands.

Macro functions

Vimspector also provides the following syntax for calling specific “macro” functions: ${Name(arg0,arg1,...)}.

The following macro functions are provided:

  • ${PickProcess(...)}: Ask the user to select a process and return its PID. If a custom PID picker is installed, the arguments are passed to it. Otherwise, a single (optional) argument is allowed, the name of the binary to find processes for. See the main Vimspector README for examples of how this is used.

It’s not possible to define your own macros or call any other functions.

In detail: Syntax is same as a braced variable, but with trailing parentheses. The contents between the parentheses must result in the inner contents of a valid JSON list, that is it must be valid to take the inner contents of the parentheses and wrap them in [ and ], and the result must be a valid JSON list. This list is then used as arguments to the macro. Don’t forget that as your expansion is actually part of some existing JSON string, you must escape any double quotes within the arguments!

For example:

"configurations": {
  "Test": {
    "configuration": {
      // Just an example, there is no FooBar macro
      "FooBar": "${FooBar(\"foo\", 10, {\"bar\": \"baz\"})}"
    }
  }
}

The splat operator

Often we want to create a single .vimspector.json entry which encompasses many use cases, as it is tedious to write every use case/start up option in JSON. This is why we have the replacement variables after all.

Frequently debug adapters request command arguments as a JSON array, for example:

    "args": [ "one", "two three", "four" ],

To help with this sort of case, Vimspector supports a ‘splat’ operator for replacement variables operating within lists. The syntax is: "*${var}, which means roughly “splice the contents of ${var} into the list at this position”. ${var} is parsed like a shell command (using python’s shlex parser) and each word is added as a list item.

For example:

   "args": [ "*${CommandLineArgs}" ]

This would:

  • Ask the user to provide the variable CommandLineArgs. Let’s say they entered one "two three" four
  • Split CommandLineArgs like shell arguments: one, two three and four
  • Set args in the settings dict to: [ "one", "two three", "four" ]

You can also combine with static values:

   "args": [ "First", "*${CommandLineArgs}", "Last" ]

This would yield the intuitive result: [ "First", "one", "two three", "four", "Last" ]

Default values

You can specify replacements with default values. In this case if the user has not specified a value, they are prompted but with the default value pre-populated, allowing them to just press return to accept the default.

The syntax is ${variableName:default value}. The default value can contain any character, but to include a } you must escape it with a backslash. To include a backslash in the JSON you must write \\, as in:

  { "key": "${value:default {\\} stuff}" }

The default value can also be a replacement variable. However, this must be a variable that’s already defined, such as one of the predefined variables, or one specified in a variables block. In order to reference them, you must use ${var} syntax and you must escape the closing }. For example, the is a common and useful case:

  {
    "configuration": {
      "program": "${script:${file\\}}"
    }
  }

This will prompt the user to specify script, but it will default to the path to the current file.

Coercing Types

Sometimes, you want to provide an option for a boolean parameter, or want to allow the user to specify more than just strings. Vimspector allows you to do this, ensuring that the resulting JSON is valid. This is done by interpreting a value as a JSON string and substituting the resulting JSON value in its place.

This is easier to explain with an example. Let’s say we want to offer the ability to break on entry, as an option for the user. The launch configuration requires stopOnEntry to be a bool. This doesn’t work:

  "stopOnEntry": "${StopOnEntry}"

The reason is that if the user types true, the resulting object is:

  "stopOnEntry": "true"

The problem being that is a string, not a boolean. So Vimspector allows you to re-interpret the string as a JSON value and use that instead. To do this, add #json to the key’s name. You can even add a default, like this:

  "stopOnEntry#json": "${stopOnEntry:true}"

If the user accepts the default, the resulting string "true" is coerced to a JSON value true, and the suffix is stripped fom the key, resulting in the following:

  "stopOnEntry": true

Which is what we need.

If you happen to have a key that already ends in #json (unlikely!), then you can force Vimspector to treat the value as a string by appending #s, as in:

  "unlikelyKeyName#json#s": "this is a string, not JSON data"

Advanced usage:

The most common usage for this is for number and bool types, but it works for objects too. If you want to be able to specify a whole object (e.g. a whole env dict), then you can do that too:

  "env#json": "${Environment:{\\}}"

The default value here is {} (note the } must be escaped!). The user can then enter something like { "MYVAR": "MyValue", "OTHER": "Other" } and the resulting object would be:

  "env": {
    "MYVAR": "MyValue",
    "OTHER": "Other"
  }

It also works for lists, though the splat operator is usually more convenient for that.

Configuration Format

All Vimspector configuration is defined in a JSON object. The complete specification of this object is available in the JSON Schema, but the basic format for the configuration object is:

{
  "adapters": { <object mapping name to <adapter configuration> },
  "configurations": { <object mapping name to <debug configuration> }
}

The adapters key is actually optional, as <adapter configuration> can be embedded within <debug configuration>, though this is not recommended usage.

Files and locations

The above configuration object is constructed from a number of configuration files, by merging objects in a specified order.

In a minimal sense, the only file required is a .vimspector.json file in the root of your project which defines the full configuration object, but it is usually useful to split the adapters configuration into a separate file (or indeed one file per debug adapter).

The following sections describe the files that are read and use the following abbreviations:

  • <vimspector home> means the path to the Vimspector installation (such as $HOME/.vim/pack/vimspector/start/vimspector), or the value of g:vimspector_base_dir if that’s set.
  • <OS> is either macos or linux depending on the host operating system.
  • <filetype> is the Vim filetype. Where multiple filetypes are in effect, typically all filetypes are checked.

Adapter configurations

Vimspector reads a series of files to build the adapters object. The adapters objects are merged in such a way that a definition for an adapter named example-adapter in a later file completely replaces a previous definition.

  • The contents of g:vimspector_adapters vim variable (dict)
  • <vimspector home>/gadgets/<OS>/.gadgets.json - the file written by install_gadget.py and not usually edited by users.
  • <vimspector home>/gadgets/<OS>/.gadgets.d/*.json (sorted alphabetically). These files are user-supplied and override the above.
  • The first such .gadgets.json file found in all parent directories of the file open in Vim.
  • The .vimspector.json and any filetype-specific configurations (see below)

In all cases, the required format is:

{
  "$schema": "https://puremourning.github.io/vimspector/schema/gadgets.schema.json#",
  "adapters": {
    "<adapter name>": {
      <adapter configuration>
    }
  }
}

Each adapters block can define any number of adapters. As mentioned, if the same adapter name exists in multiple files, the last one read takes precedence and completely replaces the previous configuration. In particular that means you can’t just override one option, you have to override the whole block.

Adapter configurations are re-read at the start of each debug session.

The specification for the gadget object is defined in the gadget schema.

Extending adapters

You can specify a key extends in the adapter configuration to inherit from an existing adapter. This is useful for defining adapters for remote debugging or where there are common options, variables etc. For example:

{
  "adapters": {
    "my-custom-debugpy": {
      "extends": "debugpy",
      "command": [ "python3", "-m", "debugpy" ]
    }
  }
}

Details of the “override” behaviour are specified below.

Debug configurations

You can define per-project or global per-filetype configurations. You can further restrict project configurations by filetype.

Project and global configurations

There are two locations for debug configurations for a project:

  • g:vimspector_configurations vim variable (dict)
  • <vimspector home>/configurations/<OS>/<filetype>/*.json
  • .vimspector.json in the project source

Typically, the debug configurations are read from .vimspector.json. The file is found (like .gadgets.json above) by recursively searching up the directory hierarchy from the directory of the file open in Vim. The first file found is read and no further searching is done.

Only a single .vimspector.json is read. If one is found, the location of this file is used for ${workspaceRoot} and other workspace-relative paths.

In addition, users can create filetype-specific configurations in the Vimspector installation directory. This can be useful where the parameters for the debug session for a particular filetype are always known in advance, or can always be entered by the user. This allows for debugging to “just work” without any modification to the project source (no need to add a .vimspector.json). In this case, the ${workspaceRoot} and workspace-relative paths are interpreted relative to the file open in Vim. This isn’t ideal, but there is no other obvious way to default this variable.

As with gadgets, any debug configurations appearing within .vimspector.json override any that appear in the common configuration dir.

Debug configurations are re-read at the start of each debug session, so modifications are picked up without any restarts of Vim.

The specification for the gadget object is defined in the schema, but a typical example looks like this:

{
  "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json#",
  "configurations": {
    "<configuration name>": {
      "adapter": "<adapter name>",
      "filetypes": [ /* list of filetypes this applies to */ ],
      "configuration": {
        "request": "<launch or attach>",
        <debug configuration>
      }
    }
  }
}

Ad-hoc configurations

You can tell vimspector to ignore any configuration files on disk, and just use a set of supplied ‘ad-hoc’ configurations. You do with as follows:

call vimspector#LaunchWithConfigurations( dict )

dict is the contents of the configurations block as a vim dictionary.

For example:

   let pid = <some_expression>
   call vimspector#LaunchWithConfigurations( {
               \  "attach": {
               \    "adapter": "netcoredbg",
               \    "configuration": {
               \      "request": "attach",
               \      "processId": pid
               \    }
               \  }
               \ } )

This would launch the debugger and attach to the specified process without the need to have a local .vimspector file on disk. The ${workspaceRoot} variable will point to the parent folder of the file that is currently open in vim.

Configuration selection

When starting debugging, you can specify which debug configuration to launch with call vimspector#LaunchWithSettings( #{ configuration: 'name here' } ).

Otherwise, vimspector tries to work out which one to laucnh.

First it finds the configurations for the current filetype from the files, or ad-hoc dictionary, mentioned above. Configurations are ignored if they specify a list of filetypes and the list doesn’t contain the one of the current buffer’s filetypes. (NOTE: the filetypes are Vim filetypes)

If there’s only one configuration found for the current filetypes, Vimspector will use that configuration, unless it contains a key "autoselect": false.

If any single configuration is found with "default": true, that one is used.

Otherwise, the user is prompted to select a configuration to use.

Specifying a default configuration

As noted, you can specify a default configuration with "default": true:

{
  "configurations": {
    "use this one": {
      "default": true,
      "adapter": " ... ",
      "configuration": {
        // ...
      }
    },
    "don't use this one": {
      // ...
    }
  }
}

If multiple configurations are found for the current filetype with default set to true, then the user is prompted anyway.

Preventing automatic selection

If you don’t want a configuration to be selected automatically, then set "autoselect": false. This particularly useful for configurations in the central (as opposed to project-local) directory. For example:

  "configurations": {
    "Don't use this by default!": {
      "autoselect": false,
      "adapter": " ... ",
      "configuration": {
        // ...
      }
    }
  }

Setting autoselect to false overrides setting default to true.

Default per filetype

If you have a number of different types of files, say some Python and some javascript/node, you can specify the filetypes list in the configuration section. As noted above, vimsepector will filter the list of configurations based on the filetypes of the current buffer. If the filetypes entry is not provided, it’s assume to apply to all buffer filetypes.

Example:

{
  "configurations": {
    "Node": {
      "adapter": "vscode-node",
      "filetypes": [ "javascript" ],
      "default": true,
      "configuration": {
        "request": "launch",
        "protocol": "auto",
        "stopOnEntry": true,
        "console": "integratedTerminal",
        "program": "${workspaceRoot}/test.js",
        "cwd": "${workspaceRoot}"
      }
    },
    "Python": {
      "adapter": "debugpy",
      "filetypes": [ "python" ],
      "default": true,
      "configuration": {
        "request": "launch",
        "program": "${workspaceRoot}/test.py",
        "stopOnEntry": true,
        "cwd": "${workspaceRoot}"
      }
    }
  }
}

Note the filetypes in the list are Vim filetypes (see :help filetype). To check the filetypes for the current buffer, look at :set filetype?.

Exception Breakpoints

Debug adapters have arbitrary configuration for exception breakpoints. Normally this is presented as a series of question to the user on starting the debug session. The question includes the name of the exception breakpoint option, the default and the list of valid responses (usually Y or N).

You can pre-configure the answers to these questions in the breakpoints section of the debug configuration. For each question, take the name provided and configure the response exception mapping in the breakpoints mapping. If the configured response is empty string, the debug adapter default will be used.

Referring to the above example, the following tells the debug adapter to use the default value for caught exceptions and to break on uncaught exception:

{
  "configurations": {
    "example-debug-configuration": {
      "adapter": "example-adapter-name",
      "breakpoints": {
        "exception": {
          "caught": "",
          "uncaught": "Y"
        }
      },
      ...

The keys in the exception mapping are what Vimspector includes in the prompt. For example, when prompted with the following:

cpp_throw: Break on C++: on throw (Y/N/default: Y)?

The exception breakpoint “type” is cpp_throw and the default is Y.

Similarly:

cpp_catch: Break on C++: on catch (Y/N/default: N)?

The exception breakpoint “type” is cpp_catch and the default is N.

Use the following to set the values in configuration and not get asked:

  "configurations": {
    "example-debug-configuration": {
      "adapter": "example-adapter-name",
      "breakpoints": {
        "exception": {
          "cpp_throw": "Y",
          "cpp_catch": "Y"
        }
      },

To just accept the defaults for these exception breakpoint types, don’t specify a value, as in :

  "configurations": {
    "example-debug-configuration": {
      "adapter": "example-adapter-name",
      "breakpoints": {
        "exception": {
          "cpp_throw": "",
          "cpp_catch": ""
        }
      },

Extending configurations

Like adapters, you can include a extends key in the configuration specification, which makes it “inherit” from the specified configuration. In practice that means any keys specified in this configuration override the corresponding keys in the “base” configuration.

This is useful in particular where there data that need to be the same across a number of debug configurations, such as environment variables, paths, variables, etc. For example:

{
  "configurations":  {
    "run - simple": {
      "adapter": "CodeLLDB",
      "configuration": {
        "program": "${workspaceRoot}/myapp",
        "env": {
          "BASEPATH": "${workspaceRoot}"
        },
        "args": []
      }
    },
    // run with some fixed options
    "run - test mode": {
      "extends": "run - simple",
      "configuration": {
        "args": [ "-mode", "test" ]
      }
    },
    // run with some different environment vars
    "run - verbose mode": {
      "extends": "run - test mode",
      "configuration": {
        "env": {
          "LOGLEVEL": "verbose"
        }
      }
    }
  }
}

These are just examples, hopefully you get the idea.

Override syntax

As exemplified above, the override is recursive, in the sense that a key like { "top": { "middle": { "bottom": "value" } } } only sets the “bottom” value and leaves all other “top” and “middle” keys untouched.

Keys can be removed from base configurations by naming the key "!<key name to delete>" and setting the value to “REMOVE”.

For example:

{
  "configurations": {
    "base" : {
      "key1": "This key is unchanged",
      "key2": {
        "key2.1": "This key is overridden",
        "key2.2": "This key is unchanged"
      },
      "key3": "This key is removed"
    },

    "derived": {
      "extends": "base",
      "key2": {
        // override key2.key2.1 in the base config
        "key2.1": "This is the override value"
      },
      // remove "key3" from the resulting config
      "!key3": "REMOVE"
    }
  }
}

The resulting “derived” configuraition ends up like this:

{
  "key1": "This key is unchanged",
  "key2": {
    "key2.1": "This is the override value",
    "key2.2": "This key is unchanged"
  }
}

Predefined Variables

The following variables are provided:

  • ${dollar} - has the value $, can be used to enter a literal dollar
  • $$ - a literal dollar
  • ${workspaceRoot} - the path of the folder where .vimspector.json was found
  • ${workspaceFolder} - the path of the folder where .vimspector.json was found
  • ${gadgetDir} - path to the OS-specific gadget dir (<vimspector home>/gadgets/<OS>)
  • ${file} - the current opened file
  • ${relativeFile} - the current opened file relative to workspaceRoot
  • ${relativeFileDirname} - path of opened file relative to workspaceRoot
  • ${fileBasename} - the current opened file’s basename
  • ${fileBasenameNoExtension} - the current opened file’s basename with no file extension
  • ${fileDirname} - the current opened file’s dirname
  • ${fileExtname} - the current opened file’s extension
  • ${cwd} - the current working directory of the active window on launch
  • ${unusedLocalPort} - an unused local TCP port

Remote Debugging Support

Vimspector has in-built support for executing remote debuggers (such as gdbserver, debugpy, llvm-server etc.). This is useful for environments where the development is done on one host and the runtime is some other host, account, container, etc.

In order for it to work, it is preferred to have set up paswordless SSH between the local and remote machines/accounts. Then just tell Vimspector how to remotely launch and/or attach to the app. By default, ‘ssh’ command is being used. Optionally, the custom ssh command can be specified with cmd property which gives opportunity to use, e.g. sshpass, for non-interactive password passing to actual ssh client (see the Python (debugpy) Example below).

This is presented as examples with commentary, as it’s a fairly advanced/niche case. If you’re not already familiar with remote debugging tools (such as gdbserver) or not familiar with ssh or such, you might need to independently research that.

Vimspector’s tools are intended to automate your existing process for setting this up rather than to offer batteries-included approach. Ultimately, all Vimspector is going to do is run your commands over SSH, docker, or locally, and co-ordinate with the adapter.

Python (debugpy) Example

Here is some examples using the Vimspector built-in remote support (using SSH) to remotely launch and attach a python application and connect to it using debugpy.

The usage pattern is to hit <F5>, enter host (the host where your app runs), account (the account it runs under), and port (a port that will be opened on the remote host). Vimspector also supports exec’ing into Docker run containers with container (the container name or id your app is running in). Vimspector then orchestrates the various tools to set you up.


{
  "adapters": {
    "python-remote": {
      "port": "${port}",
      "host": "${host}",
      "launch": {
        "remote": {
          "host": "${host}", // Remote host to ssh to
                             // If omitted, runCommand(s) is run locally
          "account": "${account}", // User to connect as (optional)

          // Optional.... Manual ssh client and additional arguments
          // "ssh": {
          //   "cmd": [ "sshpass", "-p", "pa$$w0rd", "ssh" ],
          //   "args": [ "-o", "StrictHostKeyChecking=no" ]
          // },

          // Command to launch the debuggee and attach the debugger;
          // %CMD% replaced with the remote-cmdLine configured in the launch
          // configuration. (mandatory)
          "runCommand": [
            "python", "-m", "debugpy",
            "--listen", "0.0.0.0:${port}",
            "--wait-for-client",
            "%CMD%"
          ]

          // Optional alternative to runCommand (if you need to run multiple
          // commands)
          // "runCommands":  [
          //   [ /* first command */ ],
          //   [ /* second command */ ]
          // ]

        }

        // optional delay to wait after running runCommand(s). This is often
        // needed because of the way docker handles TCP, or if you're using some
        // wrapper (e.g. to start the JVM)
        // "delay": "1000m" // format as per :help sleep
      },
      "attach": {
        "remote": {
          "host": "${host}", // Remote host to ssh to
                             // If omitted, runCommand(s) is run locally
          "account": "${account}", // User to connect as (optional)
          // Command to get the PID of the process to attach  (mandatory)
          "pidCommand": [
            //
            // Remember that you can use ${var} to ask for input. I use this to
            // call a custom command to return the PID for a named service, so
            // here's an examle:
            //
            "/path/to/secret/script/GetPIDForService", "${ServiceName}"
          ],

          // Command to attach the debugger; %PID% replaced with output of
          // pidCommand above (mandatory)
          "attachCommand": [
            "python", "-m", "debugpy", "--listen", "0.0.0.0:${port}",
            "--pid", "%PID%"
          ]

          // Optional alternative to attachCommand (if you need to run multiple
          // commands)
          // "attachCommands":  [
          //   [ /* first command */ ],
          //   [ /* second command */ ]
          // ],

          // Optional.... useful with buggy gdbservers to kill -TRAP %PID%
          // "initCompleteCommand": [
          //   /* optional command to run after initialized */
          // ]

          // Optional.... Manual ssh client and additional arguments
          // "ssh": {
          //   "cmd": [ "sshpass", "-p", "pa$$w0rd", "ssh" ],
          //   "args": [ "-o", "StrictHostKeyChecking=no" ]
          // },
        }
        // optional delay to wait after running runCommand(s). This is often
        // needed because of the way docker handles TCP, or if you're using some
        // wrapper (e.g. to start the JVM)
        // "delay": "1000m" // format as per :help sleep
      }
    }
  },
  "configurations": {
    "remote-launch": {
      "adapter": "python-remote",

      "remote-request": "launch",
      "remote-cmdLine": [
        "${RemoteRoot}/${fileBasename}", "*${args}"
      ],

      "configuration": {
        "request": "attach",
        "pathMappings": [
          {
            "localRoot": "${workspaceRoot}",
            "remoteRoot": "${RemoteRoot}"
          }
        ]
      }
    },
    "remote-attach": {
      "variables": {
        // Just an example of how to specify a variable manually rather than
        // vimspector asking for input from the user
        "ServiceName": "${fileBasenameNoExtension}"
      },

      "adapter": "python-remote",
      "remote-request": "attach",

      "configuration": {
        "request": "attach",
        "pathMappings": [
          {
            "localRoot": "${workspaceRoot}",
            "remoteRoot": "${RemoteRoot}"
          }
        ]
      }
    }
  }
}

C-family (gdbserver) Example

This example uses Vimspector to remotely launch or attach to a binary using gdbserver and then instructs vscode-cpptools to attach to that gdbserver.

The approach is very similar to the above for python, just that we use gdbserver and have to tell cpptools a few more options.

{
  "adapters": {
    "cpptools-remote": {
      "command": [
        "${gadgetDir}/vscode-cpptools/debugAdapters/bin/OpenDebugAD7"
      ],
      "name": "cppdbg",
      "configuration": {
        "type": "cppdbg" },
      "launch": {
        "remote": {
          "host": "${host}",
          "account": "${account}",
          // or, alternatively "container": "${ContainerID}"

          "runCommand": [
            "gdbserver",
            "--once",
            "--no-startup-with-shell",
            "--disable-randomization",
            "0.0.0.0:${port}",
            "%CMD%"
          ]
        },
        "delay": "1000m" // optional
      },
      "attach": {
        "remote": {
          "host": "${host}",
          "account": "${account}",
          // or, alternatively "container": "${ContainerID}"

          "pidCommand": [
             // e.g. "/path/to/secret/script/GetPIDForService", "${ServiceName}"
             // or...
             "pgrep", "executable"
          ],
          "attachCommand": [
            "gdbserver", "--once",
            "--attach",
            "0.0.0.0:${port}",
            "%PID%"
          ]
          //
          // If your application is started by a wrapper script, then you might
          // need the following. GDB can't pause an application because it only
          // sends the signal to the process group leader. Or something.
          // Basically, if you find that everything just hangs and the
          // application never attaches, try using the following to manually
          // force the trap signal.
          //
          // "initCompleteCommand": [
          //   "kill",
          //   "-TRAP",
          //   "%PID%"
          // ]
        },
        "delay": "1000m" // optional
      }
    }
  },
  "configurations": {
    "remote launch": {
      "adapter": "cpptools-remote",
      "remote-cmdLine": [ "/path/to/the/remote/executable", "args..." ],
      "remote-request": "launch",
      "configuration": {
        "request": "launch",

        "program": "/path/to/the/local/executable",
        "cwd": "${workspaceRoot}",
        "MIMode": "gdb",
        "miDebuggerServerAddress": "${host}:${port}",
        "sourceFileMap#json": "{\"${RemoteRoot}\": \"${workspaceRoot}\"}"
      }
    },
    "remote attach": {
      "adapter": "cpptools-remote",
      "remote-request": "attach",
      "configuration": {
        "request": "launch",

        "program": "/path/to/the/local/executable",
        "cwd": "${workspaceRoot}",
        "MIMode": "gdb",
        "miDebuggerServerAddress": "${host}:${port}",
        "sourceFileMap#json": "{\"${RemoteRoot}\": \"${workspaceRoot}\"}"
      }
    }
  }
}

Docker Example

This example uses Vimspector to remotely launch or attach to a docker container port.

{
  "adapters": {
    "python-remote": {
      "port": "${port}",
      "launch": {
        "remote": {
          "container": "${container}", // Docker container id or name to exec into to.

          // Command to launch the debuggee and attach the debugger;
          // %CMD% replaced with the remote-cmdLine configured in the launch
          // configuration. (mandatory)
          "runCommand": [
            "python", "-m", "debugpy",
            "--listen", "0.0.0.0:${port}",
            "--wait-for-client",
            "%CMD%"
          ]

          // Optional alternative to runCommand (if you need to run multiple
          // commands)
          // "runCommands":  [
          //   [ /* first command */ ],
          //   [ /* second command */ ]
          // ]

        }

        // optional delay to wait after running runCommand(s). This is often
        // needed because of the way docker handles TCP
        "delay": "1000m" // format as per :help sleep
      },
      "attach": {
        "remote": {
          "container": "${container}", // Docker container id or name to exec into.
          // Command to get the PID of the process to attach  (mandatory)
          // This command gets appended to "docker exec ${container}"
          "pidCommand": [
            //
            // Remember that you can use ${var} to ask for input. I use this to
            // call a custom command to return the PID for a named service, so
            // here's an examle:
            //
            "sh", "-c", "pgrep", "-f", "${filename}"
          ],

          // Command to attach the debugger; %PID% replaced with output of
          // pidCommand above (mandatory)
          "attachCommand": [
            "sh", "-c", "python", "-m", "debugpy", "--listen", "0.0.0.0:${port}",
            "--pid", "%PID%"
          ]

          // Optional alternative to attachCommand (if you need to run multiple
          // commands)
          // "attachCommands":  [
          //   [ /* first command */ ],
          //   [ /* second command */ ]
          // ],

          // Optional.... useful with buggy gdbservers to kill -TRAP %PID%
          // "initCompleteCommand": [
          //   /* optional command to run after initialized */
          // ]

        }

        // optional delay to wait after running runCommand(s). This is often
        // needed because of the way docker handles TCP, or if you're using some
        // wrapper (e.g. to start the JVM)
        "delay": "1000m" // format as per :help sleep
      }
    }
  },
  "configurations": {
    "remote-launch": {
      "adapter": "python-remote",

      "remote-request": "launch",
      "remote-cmdLine": [
        "${RemoteRoot}/${fileBasename}", "*${args}"
      ],

      "configuration": {
        "request": "attach",
        "pathMappings": [
          {
            "localRoot": "${workspaceRoot}",
            "remoteRoot": "${RemoteRoot}"
          }
        ]
      }
    },
    "remote-attach": {
      "variables": {
        // Just an example of how to specify a variable manually rather than
        // vimspector asking for input from the user
        "FileName": "${fileName}"
      },

      "adapter": "python-remote",
      "remote-request": "attach",

      "configuration": {
        "request": "attach",
        "pathMappings": [
          {
            "localRoot": "${workspaceRoot}",
            "remoteRoot": "${RemoteRoot}"
          }
        ]
      }
    }
  }
}

Appendix: Configuration file format

The configuration files are text files which must be UTF-8 encoded. They contain a single JSON object, along with optional comments.

Comments are “c-style”, i.e.:

  • // single line comment ...
  • /* inline comment */

There is much debate about whether JSON files should contain comments. I have added them because they are useful in the context of configuration files. Unforutnately this may mean your editor doesn’t like them (they are strictly invalid JSON) so it’s up to you if you use them.

Technically, Vimspector uses JSON minify to strip comments before parsing the JSON.

Appendix: Editor configuration

If you would like some assistance with writing the JSON files, and your editor of choice has a way to use a language server, you can use the VSCode JSON language server.

It is recommended to include the $schema declaration as in the above examples, but if that isn’t present, the following JSON language server configuration is recommended to load the schema from the Internet:

{
  "json": {
    "schemas": [
      {
        "fileMatch": [ ".vimspector.json" ],
        "url": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json"
      },
      {
        "fileMatch": [ ".gadgets.json", ".gadgets.d/*.json" ],
        "url": "https://puremourning.github.io/vimspector/schema/gadgets.schema.json"
      }
    ]
  }
}

If your language server client of choice happens to be YouCompleteMe, then the following .ycm_extra_conf.py is good enough to get you going, after following the instructions in the lsp-examples repo to get the server set up:

VIMSPECTOR_HOME = '/path/to/vimspector' # TODO: Change this

def Settings( **kwargs ):
  if kwargs[ 'language' ] == 'json':
    return {
      'ls': {
        'json': {
          'schemas': [
            {
              'fileMatch': [ '.vimspector.json' ],
              'url': f'file://{VIMSPECTOR_HOME}/docs/schema/vimspector.schema.json'
            },
            {
              'fileMatch': [ '.gadgets.json', '.gadgets.d/*.json' ],
              'url': f'file://{VIMSPECTOR_HOME}/docs/schema/gadgets.schema.json'
            }
          ]
        }
      }
    }

  return None # Or your existing Settings definition....

This configuration can be adapted to any other LSP-based editor configuration and is provided just as an example.