IDE and DAP integration#

This chapter covers editor-driven debugging: Perl::LanguageServer in VS Code and its counterparts in Neovim / Emacs / IntelliJ, remote-over-SSH debugging, and container-mode debugging via kubectl or docker.

Readers who reach for an IDE rather than a terminal prompt will find here the minimum viable setup, the config keys that matter, and the known fragility points (Coro, path mapping, attach vs launch).

The landscape#

Two wire protocols, both with Perl backends:

Protocol

Perl backend

Status

DAP

Perl::LanguageServer

Active.

DBGp

Devel::Debug::DBGp

Stagnant since 2018.

For new setups, use DAP via Perl::LanguageServer. DBGp survives only where an existing client (vim vdebug, pugdebug) is already in use.

Perl::LanguageServer bundles an LSP (code intelligence, diagnostics, go-to-definition) and a DAP (debugger) adapter in one process. The LSP half works on any Perl file in the editor; the DAP half runs only during a debug session.

Minimal VS Code setup#

CPAN side — install into the Perl the debuggee will run under:

cpanm Perl::LanguageServer

Editor side — install the richterger.perl extension:

Ctrl-P  →  ext install richterger.perl

.vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "perl",
      "request": "launch",
      "name": "Debug current file",
      "program": "${workspaceFolder}/${relativeFile}",
      "stopOnEntry": true,
      "args": [],
      "cwd": "${workspaceFolder}",
      "env": {},
      "reloadModules": true
    }
  ]
}

Open a .pl or .pm, set breakpoints in the gutter, press F5. The Debug Console accepts any Perl expression, evaluated in the current stack frame.

.vscode/settings.json for @INC and interpreter selection:

{
  "perl.perlInc": [
    "${workspaceFolder}/lib",
    "${workspaceFolder}/local/lib/perl5"
  ],
  "perl.perlCmd": "/usr/bin/perl"
}

Capabilities#

Perl::LanguageServer supports the DAP features that matter:

  • Launch, pause, step in / over / out, return.

  • Conditional and unconditional breakpoints, added at any time.

  • Variable inspection across stack frames.

  • Set variable — change a value mid-session from the editor.

  • Watch expressions.

  • Evaluate-in-context at the Debug Console.

  • Module hot-reload via reloadModules: true.

Not supported:

  • Attach to a running process. request: "attach" is not implemented; only "launch" (the LS starts the debuggee itself).

Remote debugging over SSH#

Edit locally; debug on a remote host. settings.json:

{
  "perl.sshCmd": "ssh",
  "perl.sshAddr": "deploy@host.example",
  "perl.sshUser": "deploy",
  "perl.sshWorkspaceRoot": "/srv/app",
  "perl.pathMap": [
    ["/srv/app", "${workspaceFolder}"]
  ]
}

Perl::LanguageServer runs on the remote host; the editor stays local. pathMap translates filenames the debuggee reports (/srv/app/lib/X.pm) to local editor paths (${workspaceFolder}/lib/X.pm).

Breakpoints that silently fail to bind are almost always a pathMap mismatch.

Container-mode debugging#

launch.json accepts container keys that invoke docker, podman, docker-compose, or kubectl:

Key

Values

containerCmd

"docker", "docker-compose", "podman", "kubectl"

containerMode

"run" (fresh container) or "exec" (existing)

containerName

Image (for run) or container name (for exec)

containerArgs

Extra args passed to the container runtime.

pathMap

[["/in/container", "/on/host"], ...]

Attach to an already-running Docker container:

{
  "type": "perl",
  "request": "launch",
  "name": "Debug in container",
  "program": "/app/bin/worker.pl",
  "containerCmd": "docker",
  "containerMode": "exec",
  "containerName": "my-running-worker",
  "pathMap": [["/app", "${workspaceFolder}"]]
}

Kubernetes pod:

{
  "type": "perl",
  "request": "launch",
  "name": "Debug in pod",
  "program": "/app/bin/worker.pl",
  "containerCmd": "kubectl",
  "containerMode": "exec",
  "containerName": "my-pod-abc123",
  "containerArgs": ["-n", "staging", "--container", "app"],
  "pathMap": [["/app", "${workspaceFolder}"]]
}

Constraints:

  • Perl::LanguageServer must be installed inside the image. Bake it into the Dockerfile or build a dev-image variant.

  • pathMap is the single biggest failure mode. Breakpoints that never bind mean the debuggee reports a path that does not translate.

Other editors#

  • Neovim — use nvim-dap with Perl::LanguageServer as the adapter. The adapter spec is a short Lua snippet that points at perl -MPerl::LanguageServer -e '...'.

  • Emacsdap-mode with the same adapter.

  • IntelliJDevel::Camelcadedb + the JetBrains Perl plugin. Not DAP; proprietary protocol. Viable but outside the Perl::LanguageServer ecosystem.

  • Sublime Text — DAP clients exist via plugins; use Perl::LanguageServer as the adapter.

Special-mode launches#

launch.json keys for non-default invocation:

Key

Effect

useTaintForDebug

Injects -T into the debuggee’s perl invocation.

sudoUser

Re-execs the debuggee as that user (needs passwordless sudo).

reloadModules

Enables module hot-reload during the session.

stopOnEntry

Break at the first statement.

Gotchas#

  • BEGIN-time errors surface as DAP “output” events, not as stopped-at-breakpoint events. Use stopOnEntry: true to get a stop at all if the script dies in BEGIN.

  • Application $SIG{__DIE__} can swallow exceptions before the debugger’s handler runs. Wrap the app’s handler in an if ($^S) guard (see exceptions).

  • Watch expressions re-evaluate on every step. Avoid expensive or side-effect expressions.

  • No source maps. Perl has no concept equivalent to a JavaScript source map. Source filters (Smart::Comments) can shift reported line numbers by one; breakpoints in filtered files may land on the wrong line.

  • Future::AsyncAwait stepping. Works for simple cases; stepping through complex await chains is unreliable. Drop to perl -d for deep async investigation, or use logging.

  • Windows nativePerl::LanguageServer’s stdin handling is broken on Windows. WSL works.

  • Coro. Perl::LanguageServer relies on Coro internally; the most-cited fragility point is Coro interacting badly with unusual build configurations. If the server refuses to start, check Coro’s install log first.

Find out more#